/* Case page — Broadsheet system. Tokens in tokens.css, the block vocabulary in
   styles.css, the case shell here.

   Hearing's scaffolding — .case-hero__*, .tldr, .marker, .qa, .probs, .slide,
   .shot — left this file with TASK-72, when the page moved onto the block
   vocabulary. No case page speaks that dialect any more; the copy six old
   lab-collibra-* snapshots still need went to the archive with the laboratory
   itself (TASK-94): 99-archive/portfolio-site-lab/lab-legacy.css.

   What stays here is the case shell — .case, .case-hero's rhythm, .case-back,
   .case-foot, .case-note — plus the chapter junction at the end of the file. */

/* ---- case shell ---- */
.case { padding-bottom: var(--sp-10); }
.case .wrap { max-width: var(--maxw); }

/* ---- case hero ---- */
.case-hero { padding-block: clamp(40px, 6vw, 88px); }

/* The hero's own rhythm. The vocabulary blocks carry no outer margins — a block
   should not have to know what sits above it — so the intervals between the
   eyebrow, the title, the rule, the standfirst and the credit are stated once,
   here, where the hero is. The heavy rule is the one rule on the site that is
   accent rather than ink. */
.case-hero > .label { margin-bottom: var(--sp-4); }
.case-hero > .prose--lead { margin: 0 0 var(--sp-6); }
.case-hero > .prose--credit { margin: 0; }
/* The summary strip is a separate object, not a continuation of the credit: its
   bold rule was landing straight on the credit's last line (visible on Hearing,
   whose credit runs three lines). One interval, stated here with the rest of the
   hero's rhythm — Collibra and McKinsey have no strip in the hero, so nothing
   moves there. TASK-111. */
.case-hero > .ledger { margin-top: var(--sp-7); }

/* ---- top return (always-visible way back, above the hero) ---- */
.case-back { padding-top: var(--sp-6); }

/* The shipped link is held back until the script has upgraded it. `case-index.js`
   is deferred, so it runs after the first paint and a hard reload showed the old
   arrow and "All work" for a frame before the index trigger replaced them
   (Pavlo, 31.07). `visibility` and not `display`: the element keeps its 44px of
   space, so nothing under it moves. The `.js` flag is set inline in the head of
   each case page — with scripting off it never lands and the plain link ships as
   it always did. */
.js .case-back:not(.case-idx) { visibility: hidden; }
/* 44px is the touch height, and it is also what makes the index layer below
   neutral to the page: `case-index.js` turns this very link into a 44px plate,
   so a link that stands 29px tall without the script pushes everything under it
   down by 15px the moment the script arrives. Measured on Collibra and McKinsey
   — the two pages that never declared the height; Light and Hearing declare it
   in their own sheets, which is why they were the two that came out neutral.
   Same shape as TASK-107 P1-6 did for the footer, and here no drawn line moves,
   because the link has none. */
.case-back a {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
  font-family: var(--font-label); font-size: var(--fs-caption);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--text-muted);
  transition: color var(--dur-fast) var(--ease);
}
.case-back a:hover { color: var(--text); }

/* ══ the site index ═══════════════════════════════════════════════════════
   TASK-85 (the menu) and TASK-98 (the ride aside), authored on Light and moved
   here whole by TASK-100 so all four cases speak one behaviour from one place.
   Everything below hangs off `.case-idx`, the class `case-index.js` puts on the
   return block — so a page without the script keeps the plain link above and
   none of this applies to it.

   The top return became the way out of a 17 121px page: same place, same size,
   same type — a diamond instead of the back arrow, the word INDEX instead of
   "All work", and a menu under it. The sign borrows its character from AWMY's
   nav cube (diamond at rest, upright and filled when open); the finish is ours —
   Broadsheet tokens, radius 0, a light grey hairline, never ink.

   It stays on screen by `position: sticky` on the block itself, so the element
   never leaves the flow and the page geometry is the same to the pixel as
   before. Sticky only pins; the script measures nothing for it.

   Once the reader scrolls past the top, the folded square no longer sits over
   the running text: it rides left into the empty page margin, rolling half a
   turn, and its chrome cools away at the end of the way, leaving the bare black
   diamond. The script drives three custom properties per frame:

     --idx-ride  px, ≤0 — spring-follower position along the way left
     --idx-fade  0..1  — how far the plate's paper and hairline have cooled
     --idx-rot   deg   — the roll of the sign over the ride */

.case-back.case-idx {
  /* the one curve borrowed from AWMY: a hard start and a long settle is what
     makes the turn read as a mechanism rather than a fade. Local to this menu,
     so tokens.css keeps its two site-wide easings. */
  --ease-cube: cubic-bezier(0.7, 0, 0.2, 1);
  /* the arrival, in numbers (TASK-108). The defaults are the medians of the
     measured references in `assets/moodboards/index-menu-grid/motion.md`: a
     panel of this size arrives in 300-400ms, a row in ~200ms, and a stagger
     step of 40-60ms is what reads as one movement rather than as a queue. The
     TEMP panel writes over them while Pavlo is choosing. */
  --idx-t: 340ms;
  --idx-t-row: 200ms;
  --idx-step: 50ms;
  --idx-lead: 140ms;
  --idx-shift: 26px;
  --idx-t-out: 160ms;
  /* TASK-120 — the sheet's own measurements, all three new.

     `--idx-gap` is the air between the plate and the sheet: the sheet is its
     own box hanging under the sign, not a shelf sewn to its bottom edge.
     The other three are the scale the home page's work card is read at inside
     the sheet. On the shelf a card is set at --fs-h3 over --fs-body in 20-36px
     of padding; here it is one step down on each, which is what "a bit smaller"
     came to when it was measured rather than guessed — small enough that four
     of them fit the screen with the page still visible round them, large enough
     that the description is read and not skimmed. `--idx-w` is the sheet, and
     it is a measured number: the longest byline (McKinsey, with the agency in
     it) needs 486px on one line, so a cell needs 486 + 48 of padding and the
     sheet needs two of those plus its own outline. At 920 it did not have them
     and the byline broke over two lines on two cards out of four while the
     other two kept one — the uneven thing Pavlo pointed at. 1100 leaves 14px
     of slack on the worst row. */
  --idx-gap: 0px;
  --idx-w: 1100px;
  --idx-fs: 1.5rem;
  --idx-fs-desc: var(--fs-small);
  --idx-pad: var(--sp-5);
  position: sticky;
  /* the block carries 32px of padding above the button; pulling the stick
     point up by that much lands the plate 16px under the top edge */
  top: calc(var(--sp-4) - var(--sp-6));
  z-index: 50;
  display: flex;
  align-items: flex-start;
  /* the block spans the whole column, so while it floats it must not intercept
     what the reader is trying to click behind it */
  pointer-events: none;
}

/* The trigger keeps the removed link's box: 44px tall, the same mono caption,
   the same 8px gap (both from `.case-back a` above). What is new is a ground —
   the element now travels over running text, so it carries its own paper and a
   light grey hairline. Border-box keeps the plate at 44px, so the block above
   the hero is 76px tall and nothing below it moves. */
.case-idx .case-idx__btn {
  /* effective fade: the scroll-driven value, except while the reader is on the
     element — hover, focus and the open index bring the full contour back */
  --idx-fade-eff: var(--idx-fade, 0);
  position: relative;
  height: 44px;
  /* a floor, not a width: at rest the word decides how wide the plate is, and
     once the word steps back the floor is what leaves a square behind */
  min-width: 44px;
  /* The sign sits at a fixed distance from the plate's left edge and never
     moves: the plate grows away from it, to the right. Centred instead, it
     wandered — while the content was still narrower than the 44px floor the box
     stayed square and the growing content slid the sign left, and once the
     content passed the floor the box grew and carried it back. Measured in slow
     motion: 9px out and 9px back on every hover, which is what Pavlo saw as the
     square jumping about.

     The padding is the one number that makes a 44px touch square around a 16px
     sign, so it is written as that and not as a step off the spacing ladder.
     The plate's own hairline is part of that 44px — left out of the sum, the
     parked square came out 46px wide and stopped being square. */
  justify-content: flex-start;
  padding-inline: calc((44px - 16px) / 2 - var(--rule-hair));
  border: var(--edge-card);
  border-color: color-mix(in srgb,
    var(--rule-muted) calc(100% * (1 - var(--idx-fade-eff))), transparent);
  background: color-mix(in srgb,
    var(--bg) calc(100% * (1 - var(--idx-fade-eff))), transparent);
  transform: translateX(var(--idx-ride, 0px));
  pointer-events: auto;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              gap var(--dur-fast) var(--ease);
}

/* Hover and keyboard focus bring the full contour back — but an open index does
   not: parked and open, the sign stays the bare black diamond it was, and only
   the sheet beside it changes. The plate must not grow while the sheet is
   measured against its edge. */
.case-idx:not([data-idx-open="true"]) .case-idx__btn:hover,
.case-idx:not([data-idx-open="true"]) .case-idx__btn:focus-visible { --idx-fade-eff: 0; }

/* on the element, the hairline darkens on top of the full contour */
.case-idx:not([data-idx-open="true"]) .case-idx__btn:hover {
  border-color: var(--text-muted);
  background: var(--bg);
}

.case-idx .case-idx__lbl {
  /* one word, one line, whatever the state */
  white-space: nowrap;
  /* clipped rather than hidden, so the word can withdraw and come back without
     the plate jumping between two layouts */
  max-width: 8rem;
  overflow: hidden;
  transition: max-width var(--dur-fast) var(--ease),
              opacity var(--dur-fast) var(--ease);
}

/* ---- the sign ----
   A 16px slot in place of the 15px back arrow, and inside it a 12px square
   standing on its corner. Open turns it upright and fills it. Geometry only:
   no SVG path is drawn by hand, and only `transform` is animated. */
.case-idx .case-idx__cube {
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  color: currentColor;
  transform: rotate(var(--idx-rot, 0deg));
}

/* the sign inherits the plate's ink at rest, and the plate's paper when open */
.case-idx .case-idx__sign { color: var(--text); }

.case-idx .case-idx__cube > i {
  position: relative;
  width: 12px;
  height: 12px;
  transform: rotate(45deg);
  transition: transform 420ms var(--ease-cube);
}

.case-idx .case-idx__cube-o {
  position: absolute;
  inset: 0;
  border: var(--rule-bold) solid currentColor;
}

.case-idx .case-idx__cube-f {
  position: absolute;
  inset: 4px;
  background: currentColor;
  transform: scale(0);
  transition: transform 300ms 50ms var(--ease-cube);
}

.case-idx[data-idx-open="true"] .case-idx__cube > i { transform: rotate(0deg); }
.case-idx[data-idx-open="true"] .case-idx__cube-f { transform: scale(1); }

/* Open. The plate goes to ink whole — word and all — and the sign is knocked
   out of it. A half-inked plate was tried first at Pavlo's ask and he took it
   back the same evening: "the whole button black, black and white, and the
   square is what turns into the blinking glyph." So the state is said by the
   plate, and what changes inside it is which mark stands there.

   The transition is declared here, on the open state, so the scroll-driven
   cooling of the chrome on the way out stays as immediate as it was. */
.case-idx[data-idx-open="true"] .case-idx__btn {
  color: var(--text-invert);
  border-color: var(--bg-invert);
  background: var(--bg-invert);
  transition: background-color 200ms var(--ease), border-color 200ms var(--ease);
}

/* the word stays out and stays light: an open index no longer folds it */
.case-idx[data-idx-open="true"] .case-idx__btn { gap: 8px; }
.case-idx[data-idx-open="true"] .case-idx__lbl { max-width: 8rem; opacity: 1; }

/* ---- one slot, two marks ----
   The square and the live glyph stand on the same grid cell, so the swap moves
   nothing beside it. `--idx-jit` is the sub-pixel displacement the flick burst
   drives while the two change hands; it rests at zero. */
.case-idx .case-idx__sign {
  display: grid;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  transform: translateX(var(--idx-jit, 0px));
}

.case-idx .case-idx__sign > * { grid-area: 1 / 1; }

/* the glyph carries a right margin for running text; in the slot the plate's
   own gap does that work */
.case-idx .case-idx__glyph { width: 16px; height: 16px; margin: 0; }

.case-idx[data-idx-sign="cube"] .case-idx__glyph,
.case-idx[data-idx-sign="glyph"] .case-idx__cube { opacity: 0; }

.case-idx[data-idx-open="true"] .case-idx__sign { color: var(--text-invert); }

/* ---- the index ----
   TASK-108: where the sheet hangs is a function of the sign's state. At the top
   of the case the trigger still wears its word and has the whole column under
   it, so the sheet drops under the button with their left edges flush. Once the
   sign has parked in the margin there is nothing under it but running text, so
   the sheet goes beside it — left edge to the plate's right edge, top to top,
   which is what TASK-100 shipped. The heavy rule follows: the seam is always
   the edge the sheet hangs from.

   The script freezes the anchor at the moment of opening (`data-idx-anchor`),
   and the plate and the sheet both carry `--idx-ride` — so a reader who scrolls
   past the parking threshold with the index open sees the two travel together
   rather than the sheet jumping to the other anchor.

   The frame rides with the plate and is the clip the reveal works inside. It
   lies over the page — nothing dims, nothing reflows, the text stays where it
   was. The frame is click-through; only the sheet takes the pointer.

   `--idx-x0` is the trigger's own left edge inside the block and `--idx-plate`
   the plate's width (44px parked, as wide as the word at rest). Both are read
   once per opening by the script, neither per frame. */
/* TASK-120 — one place, under the sign, at every state of the page.

   The side anchor is gone, and the reason is the hand rather than the layout:
   a reader who has walked the pointer up to the sign carries straight on
   downward, where the old anchor made them turn a corner into the margin to
   reach the list. So `data-idx-anchor` and everything that switched on it —
   the frozen anchor, the plate measured per opening, `--idx-plate` — come out
   with it. What is left is a constant: the sheet's left edge is the button's
   left edge, and the sheet hangs `--idx-gap` under the plate as its own box,
   not as a shelf sewn to the plate's bottom edge.

   Nothing has to freeze any more either. The old sheet stood against the
   plate's right edge, so the word withdrawing or coming back moved it; this
   one stands against the button's left edge, which no state of the plate ever
   moves. The plate and the sheet still carry the same `--idx-ride`, so they
   travel into the margin together. */
.case-idx .case-idx__drop {
  position: absolute;
  /* Nothing here scrolls, and that is the decision rather than an omission. A
     capped, scrolling frame was built and taken straight back out [Паша,
     31.07]: it put a bar down the menu's side and let the sheet slide up behind
     the trigger, while the page below it stood empty. The menu fits instead —
     the short-window rules at the end of this section take the card down a step
     rather than cutting it off. Nothing clips the cell's ring either, which is
     what the slack and the negative margin here used to be for. */
  /* 32px of block padding above the plate, the 44px plate, then the air */
  top: calc(var(--sp-6) + 44px + var(--idx-gap));
  left: var(--idx-x0, 0px);
  width: fit-content;
  max-width: calc(100vw - var(--idx-x0, 0px) - var(--sp-4));
  transform: translateX(var(--idx-ride, 0px));
  pointer-events: none;
}

.case-idx .case-idx__drop[hidden] { display: none; }

/* The sheet states its own edge because it stands over running text and over
   pictures: paper and a hairline on every free side. A closed frame is off the
   system's stop-list, and this is the exception the stop-list allows: an
   overlay has to say where it ends.
   The version without the frame was on the bench and lost — over the busy pages
   the paper alone did not hold its edge.

   TASK-120: the 4px seam goes. It was the edge the sheet held on to, and with
   the sheet standing free under the plate there is nothing left for it to hold
   — a heavy rule with no junction under it is just weight, which is the half
   of "cheap" that can be measured. What states the edge now is the hairline
   alone, and whether that is enough over the busy pages is a measurement
   (crops over the same seven places TASK-108 shot), not an opinion. */
.case-idx .case-idx__sheet {
  width: var(--idx-w);
  /* the frame above caps the width to what is left of the viewport; without
     this the sheet keeps its declared width and walks off the right edge on a
     narrow laptop (measured at 1024) */
  max-width: 100%;
  /* The height is capped on the frame outside, not here: a box that scrolls is
     a box that clips, and the clip cut the hover ring off along the sheet's own
     edge — the cell lit up on its inner sides and went dark where it met the
     outline (Pavlo, 31.07: "it only lights the inner contour"). */
  background: var(--bg);
  /* The index is one sheet, not a hero object: its outer contour is the same
     thin black rule as every division inside the card grid. */
  border: var(--rule-hair) solid var(--rule);
  visibility: hidden;
  pointer-events: auto;
}

/* One box, not two. The grid brings its own outline and its own paper from the
   home page; inside the sheet the sheet is the box, so the grid keeps only the
   line that separates it from the Home row above it. */
.case-idx .case-idx__sheet .ledger--cards {
  border: 0;
  border-top: var(--rule-hair) solid var(--rule);
  background: transparent;
}

/* The lines between the cells are ink, not grey. Grey ones sat at the same
   tone and the same weight as the hairlines inside a card — the byline's and
   the foot's — so the box read as one field of thin grey rules and the cards
   stopped being separate objects (Pavlo, 31.07). Ink at the same hairline
   weight keeps the sheet light and puts the two jobs on different tones: black
   divides cards, grey divides the parts of one card. */
.case-idx .case-idx__sheet .ledger--wrap > * {
  border-left-color: var(--rule);
}
.case-idx .case-idx__sheet .ledger--wrap > :nth-child(n + 3) {
  border-top-color: var(--rule);
}

.case-idx[data-idx-open="true"] .case-idx__sheet { visibility: visible; }

/* ---- the four works ----
   TASK-120, second pass. The first one made the sheet a list of names with a
   mono byline over each; Pavlo drew what he actually wanted instead — a wide
   box under the sign, split down the middle, holding the work cards from the
   home page themselves. Not a reduction of them and not a quotation: the same
   cards, at a smaller scale.

   So nothing about a card is described here. The script builds `work__card`
   with the home page's own classes and the vocabulary in `styles.css` draws it,
   including the grid: `ledger--cards` is the 2px ink outline and the white
   paper, `ledger--wrap` is two cells across with a hairline between them and
   between the rows. That is the drawing in the sketch, and it already existed.

   What belongs to the index is only the scale the card is read at, and it is
   set here — over the vocabulary, under the page. Everything a card says is
   kept: the client and the agency it came through, the role, the years, the
   name, the description, the kind and the rubrics. Pavlo went through the list
   out loud and stopped himself at role — "there is founder there, that is the
   whole point" — so nothing is dropped from a card in the index. */

/* First, a reset — and it is not housekeeping, it is a real defect this pass
   uncovered. `.case-back a` at the top of this file sets the top return's own
   look: mono, uppercase, letter-spaced, `inline-flex` centred on its cross
   axis. It is a tag selector inside the block, so the moment the sheet holds
   real links it catches every one of them: three of the four cards came out
   with their descriptions set in mono and their contents centred (the fourth
   escaped only because the current case is rendered as a span rather than a
   link). The vocabulary's own class loses the tie at 0,1,0 against 0,1,1, so
   the card states what it is here, at 0,3,1. */
.case-idx .case-idx__sheet a.work__card-link {
  display: flex;
  align-items: stretch;
  font-family: var(--font-body);
  font-size: inherit;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
  min-height: 0;
}

/* The scale: everything is expressed against the vocabulary's own steps rather
   than as a second set of values. */
.case-idx .case-idx__sheet .work__card-link {
  gap: var(--sp-3);
  padding: var(--idx-pad);
}

.case-idx .case-idx__sheet .work__name {
  margin-bottom: 0;
  font-size: var(--idx-fs);
  line-height: var(--lh-snug);
}

.case-idx .case-idx__sheet .work__desc { font-size: var(--idx-fs-desc); }

.case-idx .case-idx__sheet .label { font-size: var(--fs-caption); }

/* `1fr` rows make every cell as tall as the tallest, which on the home page is
   right — the four cards are the section. Here the sheet hangs over a page and
   the tallest card would set a floor for a box that is already large, so a row
   is as tall as it needs to be. */
.case-idx .case-idx__sheet .work__grid,
.case-idx .case-idx__sheet .ledger {
  grid-auto-rows: auto;
  /* A case sheet may give every `.ledger` its own chapter interval. The menu's
     grid is inside that page too, but it already follows the Home row and must
     start at the seam rather than inherit the page-level margin. */
  margin-top: 0;
}

/* One card under another was on the bench and is off it again: without a scroll
   the four of them stand 1033px tall against the ~600 a window leaves under the
   trigger, so the variant could only ever be shown broken. The single-column
   rules that remain are the phone's, at the end of this section. */

/* Home is a place, not a work: it says so by being the one row set in the
   chrome's own type — mono, caption size — rather than by being a card with no
   client and no year. TASK-108 meant to separate it with a hairline and the
   rule it wrote never matched the markup (`--home` is a class the script does
   not set), so the sheet shipped from 30.07 with Home looking exactly like a
   work. Now the rule matches, and the row sits above the grid rather than in
   it — a cell of the grid was on the TASK-108 bench and lost. */
.case-idx .case-idx__home {
  display: flex;
  align-items: center;
  min-height: 44px;
  /* four more pixels above and below than the row started with: it is a control
     at the head of the sheet, not a line of small print, and it needs the air
     to read as one (Pavlo, 31.07) */
  padding: var(--sp-4) var(--idx-pad);
  font-family: var(--font-label);
  font-size: var(--fs-caption);
  font-weight: var(--wt-med);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  /* ink, not muted — it was the lightest thing in a sheet full of ink */
  color: var(--text);
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}



/* The Home row answers the pointer the way a cell does — the same ring, on all
   four sides, drawn over the sheet's own outline. It is a row of the sheet, not
   a caption, and a row of the sheet is a control. */
.case-idx a.case-idx__home:hover,
.case-idx a.case-idx__home:focus-visible {
  position: relative;
  z-index: 2;
  box-shadow: 0 0 0 var(--rule-bold) var(--accent);
  background: var(--bg);
  outline: none;
}

/* ---- the case being read ----
   Not a card that failed to become a link: the selected one. It keeps every
   word the others keep and takes a ground plus the chip that names the state
   out loud — the same chip the held card wears, so the sheet has one way of
   saying "this card is in a state" rather than two. */
/* ---- the case being read ----
   The grey ground and the chip in the middle of the card are both gone: the
   ground read as dirt rather than as a state, and a plaque halfway down a card
   is an indicator with nowhere to belong (Pavlo, 31.07). What replaces them is
   on the bench — four ideas, none of which adds a box or a fill:

     frame   the cell keeps a permanent accent outline, and hover answers in ink
             so the two states never say the same thing
     rule    an accent rule down the cell's left edge — the margin device the
             style rules call working the margin, at the scale of a cell
     byline  a word in the service row, where a card already says what it is;
             no ground, no plaque, nothing in the middle of the card
     none    nothing at all: the card simply is not a link

   Pavlo's pick (31.07): the rule. It is the quietest of the four and it does
   not argue with hover, because it is a different device rather than the same
   one in another colour. */
.case-idx .case-idx__sheet .work__card--here .work__card-link { cursor: default; }

/* ---- the hover ring is drawn by the cell, not by the link ----
   The shelf puts its accent ring on the link with a box-shadow. Inside the
   sheet the link sits within the cell's own border, so the ring came up one
   line short of the black divider that crosses at the corner and the junction
   read as a step (Pavlo, 31.07: "оно не вровень"). Drawn by the cell and pulled
   out by the hairline's width, the ring stands ON the divider: its top edge and
   the black line are the same line, and the corner closes.

   How far out each side goes is the width of the line that side sits on. Every
   line here is a hairline — the divider between cells and, since the sheet's
   contour was thinned to match it, the outer edge of the sheet too. So one
   value covers all four sides and no side needs to know where it sits.

   It used to know: the outer sides were pulled out by 2px, from when the sheet
   was drawn with a 2px contour, and which sides were outer was read off a fixed
   two-by-two grid (`nth-child`). Both halves had rotted. The 2px pull put a
   pixel of orange outside the sheet, where the frame's clip ate it, so those
   sides came up a pixel thin — and under 760px the sheet is one column, where
   `nth-child(odd)` no longer means "left edge" at all (Pavlo, 12.08: "внешние
   линии тоненькие, внутренние толстенькие"). Measured after: 2px of orange on
   all four sides, its outer edge flush with the sheet's own contour, nothing
   standing outside it.

   The ring is a layer of its own rather than a border, because a border would
   move the card's contents by its own width the moment the pointer arrived. */
.case-idx .case-idx__sheet .work__card-link:hover { box-shadow: none; }

.case-idx .case-idx__sheet .work__card {
  --ring-t: var(--rule-hair);
  --ring-r: var(--rule-hair);
  --ring-b: var(--rule-hair);
  --ring-l: var(--rule-hair);
}
/* the hovered cell comes forward, or the cell after it in the source paints its
   own divider over the ring's edge */
.case-idx .case-idx__sheet .work__card:hover,
.case-idx .case-idx__sheet .work__card:focus-within { z-index: 2; }

.case-idx .case-idx__sheet .work__card:hover::after,
.case-idx .case-idx__sheet .work__card:focus-within::after {
  content: "";
  position: absolute;
  inset: calc(var(--ring-t) * -1) calc(var(--ring-r) * -1)
         calc(var(--ring-b) * -1) calc(var(--ring-l) * -1);
  border: var(--rule-bold) solid var(--accent);
  z-index: 3;
  pointer-events: none;
}

/* The card being read still answers the pointer: it is a cell of the same list
   and it can be aimed at, so it takes the shelf's accent ring like the others
   (Pavlo, 31.07). The ring is not in competition with the rule — along the left
   edge the two simply add up and the frame reads thicker on that side. What the
   rule needs is to stay on top of the ground the hover lays down, which is what
   its own z-index below is for. */

.case-idx .work__card--here {
  position: relative;
  z-index: 1;
}

/* The rule takes its thickness inward, from the cell's own field, and never
   outward. Grown outward it stood past the corner where the black divider
   crosses, and the overhang read as a step (Pavlo, 31.07: "она должна чётко
   пройти по углу и закрыть угол, но не выступать"). Held flush to all three
   edges it ends exactly at the corner, and the black line that crosses there
   closes it.

   Sideways it is the other way round: it runs out to the far edge of the line
   it stands on and takes that line's place, so the reader sees one rule and not
   a black stripe with an orange one beside it (Pavlo, 31.07). That line is a
   hairline wherever the cell is — the divider inside the sheet and the sheet's
   own contour are the same weight — so one value serves both columns and the
   one-column layout under 760px as well. What stays fixed is the width inside
   the cell.

   Its own z-index puts it over the ground the shelf's hover lays down: without
   it the hovered cell painted the rule out. */
.case-idx .work__card--here::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  /* --edge: the line to the left of this cell — a hairline everywhere */
  --edge: var(--rule-hair);
  left: calc(var(--edge) * -1);
  width: calc(var(--rule-heavy) + var(--edge));
  background: var(--accent);
  z-index: 2;
  pointer-events: none;
}

/* The tag stands where the client's name stood: same line, same type, so the
   byline keeps its shape and only its first word changes. It is a tag rather
   than accent text because it has to read as an indicator at a glance and not
   as another company. */
.case-idx .work__card--here .work__client--here {
  display: inline-block;
  padding: 2px 6px;
  background: var(--accent);
  color: var(--text-invert);
  font-weight: var(--wt-med);
}

.case-idx .case-idx__sheet a.work__card-link:focus-visible,
.case-idx a.case-idx__home:focus-visible {
  outline: var(--rule-bold) solid var(--text);
  outline-offset: -3px;
}

/* ---- how the index arrives ----
   Two layers, the way the measured references build it (`motion.md`): the sheet
   opens downward along the seam it hangs from, and the rows come in behind it,
   one after another, from the side the sign is on. Five arrivals were on the
   bench — a drawer, a wipe from the seam, a diamond growing out of the sign, a
   corner opening on both axes — and Pavlo took the unroll (30.07).

   Motion is a mask and a shift only: no scale and no rotation, which the
   stop-list bans. A mask is neither — nothing is drawn at the wrong size at any
   point, the paper is simply not shown yet. The way out is shorter than the way
   in and has no stagger, because leaving a menu is not an event.

   The numbers are the medians of the measured references, confirmed by Pavlo on
   the bench and not to be re-tuned by eye:
     --idx-t      340ms  the sheet's own arrival
     --idx-t-row  200ms  one row
     --idx-step    50ms  between rows
     --idx-lead   140ms  how long the rows let the sheet get ahead
     --idx-shift   26px  how far a row comes from
     --idx-t-out  160ms  the way back */

.case-idx .case-idx__sheet {
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--idx-t-out) var(--ease),
              visibility 0s var(--idx-t-out);
}

.case-idx[data-idx-open="true"] .case-idx__sheet {
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--idx-t) var(--ease-out),
              visibility 0s;
}

/* TASK-120: the Home row arrives with the rest of them — it is a row of the
   sheet like any other, only set in the chrome's type instead of the work's */
.case-idx .work__card,
.case-idx .case-idx__home {
  opacity: 0;
  transform: translateX(calc(var(--idx-shift) * -1));
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

.case-idx[data-idx-open="true"] .work__card,
.case-idx[data-idx-open="true"] .case-idx__home {
  opacity: 1;
  transform: none;
  transition: opacity var(--idx-t-row) var(--ease-out),
              transform var(--idx-t-row) var(--ease-out);
  /* the delay is declared apart from the shorthand above: `--i` lives on the
     row, and a custom property that reads it has to be substituted there — on
     the block it would resolve to nothing but zero */
  transition-delay: calc(var(--idx-lead) + var(--i, 0) * var(--idx-step));
}

/* the case being read stays on the list and is not a link to itself */
/* The name of the selected card keeps its ink: the ground and the chip say
   where the reader is, and a greyed title would say the card is broken —
   which is the thing Pavlo asked for it to stop saying. */
.case-idx .case-idx__sheet [aria-current="page"] .work__name { color: var(--text); }

/* TASK-101 — a case that is held. Same treatment as the current row: text, not a
   link, and cooled off. No word beside it — Pavlo's call: the row going quiet is
   enough, and a "coming soon" tag makes the site talk about its own backlog. */
.case-idx .case-idx__sheet [aria-current="page"] { cursor: default; }

/* ---- parked: the word steps back once the page is scrolled ----
   At the top of the case the trigger reads as what it replaced — a sign and a
   word. Once the reader has left the top it has no run of empty column to sit
   in any more, so it withdraws to the sign alone and the plate closes into a
   square. The word returns on hover, on keyboard focus and while the index is
   open. This is the same bargain the NDA mark makes on the McKinsey case, so
   an element that parks itself behaves one way across the site, not two. */
.case-idx[data-idx-stuck="true"] .case-idx__btn { gap: 0; }

.case-idx[data-idx-stuck="true"] .case-idx__lbl {
  max-width: 0;
  opacity: 0;
}

/* The word comes back on hover and on keyboard focus. It does not come back on
   opening: the index now stands beside the sign instead of under it, so there is
   nothing for the word to announce and nothing to push the sheet sideways. */
.case-idx[data-idx-stuck="true"]:not([data-idx-open="true"]) .case-idx__btn:hover,
.case-idx[data-idx-stuck="true"]:not([data-idx-open="true"]) .case-idx__btn:focus-visible { gap: 8px; }

.case-idx[data-idx-stuck="true"]:not([data-idx-open="true"]) .case-idx__btn:hover .case-idx__lbl,
.case-idx[data-idx-stuck="true"]:not([data-idx-open="true"]) .case-idx__btn:focus-visible .case-idx__lbl {
  max-width: 8rem;
  opacity: 1;
}

/* TASK-120 — and an open index keeps it out at any scroll position. Declared
   with both attributes because the parked rules above match at the same weight
   and stand later in the file: at one attribute this lost the tie silently and
   the word stayed folded under an open sheet (measured). */
.case-idx[data-idx-stuck="true"][data-idx-open="true"] .case-idx__btn { gap: 8px; }
.case-idx[data-idx-stuck="true"][data-idx-open="true"] .case-idx__lbl {
  max-width: 8rem;
  opacity: 1;
}

/* TASK-120 — the four rules that froze the trigger while the index was open are
   gone with the anchor they served. They existed because the old sheet stood
   against the plate's right edge: a plate that changed width underneath an open
   sheet dragged it across the word (51px of overlap, measured by TASK-108). The
   sheet now hangs from the button's left edge, which no state of the plate
   moves, so the word may come and go under an open index without touching it. */

/* Reduced motion: the state still reads — upright and filled versus standing on
   a corner — it just arrives instead of travelling. The sheet keeps `visibility`
   so a closed index stays out of the tab order either way. The ride is off; the
   script returns before it starts. */
@media (prefers-reduced-motion: reduce) {
  .case-idx .case-idx__btn,
  .case-idx .case-idx__lbl,
  .case-idx .case-idx__cube > i,
  .case-idx .case-idx__cube-f,
  .case-idx .case-idx__sheet,
  .case-idx .case-idx__home,
  .case-idx .work__card { transition-duration: 0s; }

  .case-idx .case-idx__sheet { transition-delay: 0s; }
  /* the open state restates the whole `transition` shorthand, so its duration
     has to be put back to zero here as well or the sheet still unrolls — and the
     row's stagger is a `transition-delay` of its own, which has to go too */
  .case-idx[data-idx-open="true"] .case-idx__sheet,
  .case-idx[data-idx-open="true"] .case-idx__home,
  .case-idx[data-idx-open="true"] .work__card {
    transition-duration: 0s;
    transition-delay: 0s;
  }
}

/* ---- fitting the window ----
   The menu does not scroll, so it has to fit — and whether it fits depends on
   the page as much as on the window: Light's card carries a state chip, which
   makes its row taller, and its grid ran past the bottom of a 900px window
   where Collibra's had room to spare. A height media query cannot know that, so
   the step is chosen by measurement in `case-index.js`: the script tries the
   full card, then these, and stops at the first one that fits.

   The steps were briefly lost with the bench they stood next to, which showed
   up as `data-idx-fit="tighter"` on a 1024px window with the type still at full
   size — the attribute was set and nothing answered it. */
.case-back.case-idx[data-idx-fit="tight"] {
  --idx-fs: 1.25rem;
  --idx-pad: var(--sp-4);
}

.case-back.case-idx[data-idx-fit="tighter"] {
  --idx-fs: 1.125rem;
  --idx-fs-desc: var(--fs-caption);
  --idx-pad: var(--sp-3);
}

/* ---- next / footer ---- */
.case-foot {
  border-top: var(--rule-heavy) solid var(--rule);
  margin-top: var(--section-y);
  padding-top: var(--sp-6);
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-5) var(--sp-7); flex-wrap: wrap;
}
.case-foot__seq { display: flex; gap: var(--sp-5) var(--sp-6); flex-wrap: wrap; align-items: baseline; }
.case-foot a {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-label); font-size: var(--fs-caption); letter-spacing: var(--tr-label);
  text-transform: uppercase; border-bottom: var(--rule-hair) solid var(--text); padding-bottom: 2px;
}
.case-foot a:hover { color: var(--accent-text); border-color: var(--accent-text); }   /* was --data-blue (A9) */
.case-foot .next { font-weight: var(--wt-med); }

/* TASK-107 P1-6 · one footer for four cases. The 44px touch height was declared
   in case-light.css and case-hearing.css only, so the same two links stood 21px
   tall on Collibra and McKinsey. Where it was declared it was worse than absent:
   min-height grew the box downward and carried the border-bottom ~11px away from
   the words. So the hit area is taken by a transparent overlay — the drawn line
   stays on the type and not one box moves (same reasoning as .bleed-ground). */
.case-foot a { position: relative; }
.case-foot a::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%); height: 100%; min-height: 44px;
}
.case-foot a:focus-visible { outline: var(--rule-bold) solid var(--text); outline-offset: 4px; }
.case-note { font-family: var(--font-label); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tr-label); color: var(--caption-ink); margin: var(--sp-6) 0 0; }

/* ---- responsive ---- */

/* TASK-107 / TASK-155 · narrow windows. Tablet keeps the ride because it has a
   real side field; the phone has none, so only its 44px sign is fixed in the
   corner and the word never travels over running text. */
@media (max-width: 899px) {
  /* Unlike the old all-mobile fallback, tablet remains sticky and lets the
     measured ride finish at EDGE=16px. `case-index.js` removes the old 24px
     dead threshold on this width, so the short 768px journey still happens. */
  .case-back.case-idx {
    position: sticky;
    top: calc(var(--sp-4) - var(--sp-6));
  }

  /* The sheet takes the width of the column. Nothing else about it changes:
     what Pavlo chose on the desktop bench — a plain column of titles and
     nothing beside them — is what a phone wanted in the first place, so the two
     ended up the same page rather than two pages to keep in step. */
  /* TASK-120: the anchor attribute these two rules were qualified by no longer
     exists, so both are written against the block itself. The sheet keeps the
     air under the plate that the desktop has — a phone is where an overlay
     welded to its trigger reads worst, because there is no margin for the eye
     to rest the box against. */
  .case-idx .case-idx__drop {
    top: calc(var(--sp-6) + 44px + var(--idx-gap));
    right: var(--gutter);
    width: auto;
    max-width: none;
  }

  .case-idx .case-idx__sheet { width: auto; }
  /* one step up: the column is the whole screen here, and 18px in it reads as
     small print rather than as a title. Both classes, because the declaration
     it overrides sits on `.case-back.case-idx` — a media query buys no
     specificity, and at one class this silently lost (measured: 18px on a
     390px screen where it was meant to be 20). */
  /* one card across, whatever the bench says: two of these on a phone column
     leaves a description four words wide */
  .case-back.case-idx { --idx-pad: var(--sp-4); }
  .case-idx .case-idx__sheet .ledger { --cells: 1; }
  .case-idx .case-idx__sheet .ledger > * { border-left: 0; }
  .case-idx .case-idx__sheet .ledger > * + * { border-top: var(--rule-hair) solid var(--rule-muted); }
}

@media (max-width: 520px) {
  /* Keep the block in flow so the hero retains the same opening rhythm. Only
     the trigger and its drop become fixed; the page never jumps by the 76px
     that the original return occupies. */
  .case-back.case-idx { position: relative; top: auto; }

  .case-back.case-idx .case-idx__btn,
  .case-idx[data-idx-open="true"] .case-idx__btn {
    position: fixed;
    top: max(var(--sp-4), env(safe-area-inset-top));
    /* Four pixels off the text axis: the plate is navigation chrome, not the
       first line of the article. */
    left: calc(var(--gutter) - 4px);
    z-index: 51;
    min-width: 44px;
    padding-inline: calc((44px - 16px) / 2 - var(--rule-hair));
    border-color: var(--rule-muted);
    background: var(--bg);
    transform: none;
  }

  /* Full plate at the top; once a meaningful scroll begins, only the 44px sign
     remains. `case-index.js` also removes the state when the reader returns. */
  .case-back.case-idx[data-idx-phone-compact="true"] .case-idx__btn {
    width: 44px;
    gap: 0;
  }
  .case-back.case-idx[data-idx-phone-compact="true"] .case-idx__lbl {
    max-width: 0;
    opacity: 0;
  }
  .case-idx[data-idx-open="true"] .case-idx__sign { color: var(--text); }

  .case-idx .case-idx__drop {
    position: fixed;
    top: calc(max(var(--sp-4), env(safe-area-inset-top)) + 44px + var(--sp-2));
    bottom: max(var(--sp-2), env(safe-area-inset-bottom));
    left: calc(var(--gutter) - 4px);
    right: var(--gutter);
    width: auto;
    max-width: none;
    transform: none;
    overflow: hidden;
  }

  /* Four full work cards cannot fit a phone height. The no-scroll decision for
     the wide menu stays intact; on a phone the sheet itself is the viewport and
     scrolls without a visible bar, while the fixed sign remains the exit. */
  .case-idx .case-idx__sheet {
    height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  .case-idx .case-idx__sheet::-webkit-scrollbar { display: none; }
}

/* ══ CHAPTER RHYTHM ═══════════════════════════════════════════════════════
   One component, one interval. It used to be set three times — 208 / 148 /
   104px before the rule and 140 / 80 / 80 below it — so identical markup
   breathed at three speeds (audit A2). Collibra's values win: they are the
   ones that were reasoned about rather than accumulated.

   The block itself is `chapter` in styles.css. What lives here is the junction:
   what the interval does to whatever sits above it. */

/* The interval belongs to the head alone. A block that ends a chapter stops
   adding trailing air of its own, or the gap is paid twice and by a different
   amount on every page. */
/* Not :where() — a case sheet loads after this one and sets padding-block on the
   same block, so a zero-specificity selector loses the interval back to it. */
section:has(+ .chapter),
div:has(+ .chapter) { padding-bottom: 0; }

/* Except a tonal slab: it carries the interval inside its own ground, so the
   paper never breaks into white before the next rule. Same number, different
   box — the slab pays, the head does not. */
.sticky-chapter--paper:has(+ .chapter),
.chapter--paper:has(+ .chapter) { padding-bottom: var(--r-chapter); }
.sticky-chapter--paper + .chapter,
.chapter--paper + .chapter { margin-top: 0; }
