/* Octralis AI Workforce: component classes
   ==========================================================================
   CHECK BEFORE SHIPPING: every comment in this file must open and close
   exactly once, in order. Handover 18 shipped with a comment terminator eight
   lines early inside the container note, which put nine lines of prose into
   selector position; the parser then read on to the next brace and dropped
   .layout--single, the class twelve templates use. A stylesheet fails
   silently in exactly that way, so comment balance is worth asserting in CI
   alongside the em-dash check.
   ==========================================================================
   Copy verbatim, the way tokens.css and print.css are taken. Every value here is a
   token from tokens.css; a raw value in this file is a defect, so a later handover
   can move a token and move every surface at once.

   Padding and gap are the one exception to "every value is a token", and it is
   deliberate: the space scale is 4-based and several of these are optical rather
   than rhythmic. A card head at 26px and its rows at 13px are two densities that
   were chosen against the type, not stepped off a scale, so naming them --s-N
   would make them look interchangeable when they are not. Every colour, size and
   family IS a token. If a padding here changes, it changes here.

   Naming: block__element, block--modifier. Blocks are named after the thing on the
   page, not the screen it appears on, so .record is the same block on Home and on
   My Competencies. Names are stable between handovers: if one changes, CHANGELOG.md
   says so explicitly, because a rename with no other change is invisible in a diff.

   Nothing here needs JavaScript to lay out. HTMX swaps fragments into the regions
   marked in the templates; with the attribute ignored the same markup is a page. */

/* ---- Page frame -------------------------------------------------------- */

.surface            { background: var(--ground); }
.surface__body      { max-width: 1148px; padding: 32px 24px 40px;
                      margin-inline: auto; }
     /* ONE CONTENT CONTAINER, EVERY SURFACE, and it is HERE rather than per page.
        1148 is 1100 of content plus the 48px of padding this rule carries. Not
        full width: past about 1150 a row of eight competency cells and its state
        chip drift far enough apart that the eye loses the pairing, and a line of
        prose in a card starts needing a cap the card should not have needed.

        Two things were reported and both are fixed by the same line. The container
        was too tight, leaving more gutter than the content deserved on a wide
        screen; and different pages had different containers, so My Learning at 868
        sat inside the same masthead as a dashboard at 1080 and read as a different
        product. The masthead and the footer are full-bleed and always were: this
        rule governs the content between them only.

        A GRID TRACK IS A MEASURE TOO. Watch for grid-template-columns with a px
        maximum, minmax(0, 760px) and the like, plus justify-content: center. That
        is a per-page container wearing a different hat, and it is how day one kept
        a 760 measure after every max-width in the templates had gone. Tracks are
        minmax(0, 1fr) unless the column is a fixed rail.

        What varies inside the frame is the layout, not the width:

          .layout--with-rail    main column + a 300 rail
          .layout--single       one column, cards full width

        A card is as wide as the frame. Prose inside a wide card takes .measure so
        the line does not run to 120 characters; the card does not shrink to the
        prose, because a box that hugs a paragraph on one page and fills the frame
        on the next is the discrepancy above in miniature. */

.layout--single     { display: flex; flex-direction: column; gap: var(--s-5); }

/* Two columns where there is a rail, one where there is not. The rail is a fixed
   300px because its content is fixed; the main column takes what is left and never
   has a measure of its own , measures belong to prose, not to blocks. */
.layout--with-rail  { display: grid; grid-template-columns: minmax(0, 1fr) 300px;
                      gap: 26px; align-items: start; }
     /* the rail stays 300 at every shell width. Its content is fixed, so widening
        it only pads a card; the surplus goes to the main column */
.layout__main       { display: flex; flex-direction: column; gap: var(--s-5); }
.layout__rail       { display: flex; flex-direction: column; gap: 18px; }

@media (max-width: 900px) {
  /* The rail goes under the main column rather than shrinking. Its cards are
     secondary by placement, and at 900px there is no placement left to say it. */
  .layout--with-rail { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Page head --------------------------------------------------------- */

.page__title        { margin: 0; font-size: var(--size-display); font-weight: 600;
                      letter-spacing: -0.025em; line-height: 1.15; }
.page__intro        { margin: 10px 0 0; font-size: var(--size-body);
                      line-height: 1.65; color: var(--text-secondary);
                      text-wrap: pretty; }
     /* NO max-width here as of handover 18. A 58ch clamp inside a card that is
        wider than 58ch leaves a dead column INSIDE the card: the text stops early
        and the box carries on, which reads as the content floating rather than as
        a reading measure. Reported as "too much space on the left and right", and
        it was this, not the page.

        THE CONTAINER IS THE MEASURE. A reading surface sizes its own card to the
        line length it wants and the prose fills it, so there is no white column
        anywhere. Use .measure only on a paragraph that sits inside a box wider
        than a reading width for a reason, a full-bleed table say. */

.measure            { max-width: 66ch; }
     /* on the PARAGRAPH, never on the box. 66ch at --size-body is about 600px, so
        a full-frame card keeps a readable line and the space to its right is the
        card's own room rather than a gap between two different page widths. */

/* ---- Card -------------------------------------------------------------- */

.card               { background: var(--surface); border: 1px solid var(--line);
                      border-radius: var(--radius); }
.card__head         { padding: 20px 24px; border-bottom: 1px solid var(--line);
                      display: flex; align-items: baseline;
                      justify-content: space-between; gap: calc(var(--s-3) + 2px);
                      flex-wrap: wrap; }
.card__title        { font-size: var(--size-h3); font-weight: 600;
                      letter-spacing: -0.015em; }
.card__body         { padding: 24px 26px; }
.card__foot         { padding: var(--s-4); padding-left: 24px; padding-right: 24px;
                      font-size: var(--size-meta); line-height: 1.6;
                      color: var(--text-secondary); }

/* Sectioned rather than uniformly padded, and the three densities are the point:
   a head carries the sentence somebody reads, rows are a record, a foot holds an
   action. One padding for all three flattens them into a list of equal things. */
.card--sectioned .card__head { padding: 26px 28px; }
.card--sectioned .card__foot { padding: 18px 28px 24px; }

/* ---- Eyebrow, the small-caps mono label -------------------------------- */

.eyebrow            { font-family: var(--font-mono); font-size: var(--size-caps);
                      font-weight: 600; line-height: 1; letter-spacing: .14em;
                      color: var(--text-secondary); }

/* .eyebrow--wide is RETAINED AS A NO-OP and nothing new should use it. The audit
   found 54 uses against 3 of the base: a modifier used eighteen times as often
   as the thing it modifies is the default, and .12 against .14 on an 11px mono
   label is not a distinction anybody can see. The two values were never a
   decision , they were a drift that acquired a class name.

   It stays declared so the 54 existing uses do not have to change in one pass,
   and it sets nothing, so the label is identical either way. Delete the class
   from markup as those files are next touched for other reasons.

   line-height: 1 is load-bearing and was unstated: without it these inherit the
   body's 1.6, and a small-caps mono label at 1.6 is a line of text rather than a
   label. */
.eyebrow--wide      { /* no-op, see above */ }

/* ---- Record: the competency rows, one block on every surface ----------- */

.record__row        { padding: 13px 24px; border-bottom: 1px solid var(--line-hairline);
                      display: grid; grid-template-columns: minmax(0, 1fr) 262px 150px;
                      gap: var(--s-5); align-items: center; }
.record__row:last-child { border-bottom: none; }
.record__name       { min-width: 0; display: flex; align-items: baseline; gap: 10px; }
.record__code       { flex: none; font-family: var(--font-mono);
                      font-size: var(--size-label); font-weight: 600;
                      color: var(--text-secondary); }
.record__label      { font-size: var(--size-body-sm); line-height: 1.4; }

/* The 262px column is the track: eight 26px cells with their gaps, sized to its
   content. It is not 1fr, because a flexible track leaves the state chip stranded at the
   far right of the surplus, which is the defect this width exists to prevent. */

@media (max-width: 700px) {
  .record__row      { grid-template-columns: minmax(0, 1fr); gap: 10px; }
}

/* ---- Track: required against demonstrated ------------------------------ */

.track              { display: flex; flex-direction: column; gap: 5px; }
.track__segments    { display: flex; gap: 4px; }
.track__seg         { flex: 1; height: 11px; box-sizing: border-box; }
.track__seg--done   { background: var(--evidence); }
     /* ink means a level demonstrated and nothing else. Never progress. */
.track__seg--gap    { background: var(--gap-fill); border: 1px solid var(--gap-line);
                      background-image: repeating-linear-gradient(45deg,
                        var(--gap-line) 0 1.5px, transparent 1.5px 5px); }
.track__seg--over   { background: var(--surface);
                      border: 1.5px dashed var(--above); }
     /* ABOVE the requirement. Dashed. */
.track__seg--none   { background: var(--surface);
                      border: 1.5px dotted var(--unrecorded); }
     /* NOTHING RECORDED. Dotted. These two are not interchangeable and neither is
        the other's default: rendering above-requirement as dotted says somebody was
        never assessed on a competency they exceeded. */
.track__ticks       { display: flex; gap: 4px; }
.track__tick        { flex: 1; text-align: center; font-family: var(--font-mono);
                      font-size: var(--size-micro); font-weight: 600;
                      letter-spacing: .06em; color: var(--tick); }
.track__tick--req   { color: var(--text); }
     /* the required tick reads "▲ REQ", not "▲ L3 REQ": the longer form wrapped
        at 360px, and the level is already in the row's own caption */

/* ---- State chip -------------------------------------------------------- */

.chip               { flex: none; font-family: var(--font-mono);
                      font-size: var(--size-label); font-weight: 600;
                      line-height: 1.5; padding: 2px 8px; white-space: nowrap;
                      background: var(--surface); }
.chip--at           { border: 1px solid var(--evidence); color: var(--evidence); }
.chip--below        { border: 1px solid var(--gap-line); color: var(--gap-text);
                      background: var(--gap-fill); }
.chip--none         { border: 1.5px dotted var(--unrecorded);
                      color: var(--text-secondary); }

/* ---- Controls ---------------------------------------------------------- */

/* A button fits its label. In a flex COLUMN the default align-items is stretch, so
   an inline-block anchor grows to the widest sibling and its text sits at the left
   edge of a box the label does not fill, which is how a control stops looking like
   one. align-self and a centred text settle it at the control rather than asking
   every column that holds one to remember.

   A control meant to fill its column asks with .btn--fill, which has to undo
   align-self as well as set a width. */
/* line-height: 1 is load-bearing, and its absence was a defect since handover 14.
 * Without it a button inherits the body's 1.6 and grows: every control in the
 * product rendered 51px tall against the boards' 45, so the padding scale every
 * board was drawn against was never the height that shipped. The PADDING carries
 * a control's height; the type does not, and a control whose height moves with an
 * inherited line-height cannot be drawn at all. */
.btn                { align-self: flex-start; text-align: center;
                      display: inline-block; font-family: var(--font);
                      font-size: var(--size-body); line-height: 1;
                      font-weight: 600; text-decoration: none;
                      border-radius: var(--radius); cursor: pointer; }
.btn--fill          { align-self: stretch; width: 100%; }
/* 15px vertical, not 13. With line-height: 1 the padding IS the height, and the
   height is a requirement rather than a preference: 44px is the minimum target at
   360, so 13 + 15 + 13 = 41 fails it and 15 + 15 + 15 = 45 clears it. The boards
   drew 45 because line-height: normal was quietly adding the 4px; now the padding
   says it, which is the only form a developer can hold a file to. */
.btn--primary       { padding: 15px 24px; background: var(--t-raw);
                      border: none; border-bottom: 1px solid var(--t-edge);
                      color: var(--t-on); }
.btn--primary:hover { box-shadow: inset 0 0 0 999px rgba(19, 25, 32, 0.14); }
     /* the only rgba in this file. It is an ink wash over the tenant fill and has
        no token because it is not a colour, it is a darkening. */
.btn--primary:active{ box-shadow: inset 0 0 0 999px rgba(19, 25, 32, 0.26); }
     /* an inset wash, never a darker hex: --t-raw is the tenant's own value and one
        wash deepens a navy and a pale gold in the same direction */
/* 14 + 14 + 14 = 42 on a 14px type, one step down from the primary and still a
   comfortable target. A secondary control may be smaller than the primary; it may
   not be smaller than a finger. */
.btn--secondary     { padding: 14px 22px; background: var(--surface);
                      color: var(--text); border: 1px solid var(--unrecorded);
                      font-size: var(--size-body-sm); }
.btn--secondary:hover { background: var(--line-hairline); border-color: var(--text); }
.btn[aria-disabled="true"] { background: var(--line-hairline); color: var(--tick);
                      border: 1px solid var(--line); pointer-events: none; }

.link               { font-family: var(--font); font-size: var(--size-body-sm);
                      font-weight: 600;
                      color: var(--t-fg); text-decoration: none; }
.link:hover         { text-decoration: underline; text-underline-offset: 2px; }
/* An in-row link, at --size-meta rather than --size-body-sm. In a row whose name is
   17px and whose count is 12, a 14px control is the second-largest thing there,
   which is wrong for something tertiary. The board was right on this one, so the
   size moves to a modifier rather than the base. */
.link--inline       { font-size: var(--size-meta); }

:focus-visible      { outline: 2px solid var(--text); outline-offset: 2px; }
     /* ink, not the tenant colour: a focused primary would otherwise draw a gold
        ring on a gold fill. One ring on every control. */

/* ---- Rail cards -------------------------------------------------------- */

.rail__card         { background: var(--surface); border: 1px solid var(--line);
                      padding: 20px 22px; }
.rail__figure       { margin-top: 11px; font-size: var(--size-body-lg);
                      font-weight: 600; letter-spacing: -0.01em; }
.rail__text         { margin-top: 7px; font-size: var(--size-body-sm);
                      line-height: 1.6; color: var(--text-secondary); }
.rail__title        { margin-top: 11px; font-size: var(--size-body);
                      font-weight: 600; line-height: 1.35; }

/* ---- Status banner: one shape, four statuses --------------------------- */

.banner             { border-left: 4px solid; padding: 14px 16px; display: flex;
                      gap: var(--s-3); align-items: flex-start; }
.banner--error      { background: var(--warning-fill);
                      border-left-color: var(--warning-line); }
.banner--warn       { background: var(--gap-fill);
                      border-left-color: var(--gap-line); }
.banner--ok         { background: var(--met-fill); border-left-color: var(--met); }
.banner--info       { background: var(--surface-sunken);
                      border-left-color: var(--unrecorded); }
     /* info takes the sunken fill, not white: the fill is what makes a bar-only
        container work, and a white fill on a white card leaves the bar doing it
        alone at 1.29:1 */
.banner__label      { font-family: var(--font-mono); font-size: var(--size-caps);
                      font-weight: 600; letter-spacing: .14em; color: var(--text); }
.banner__text       { margin-top: 7px; max-width: 62ch;
                      font-size: var(--size-body-sm); line-height: 1.6;
                      color: var(--text); }
.banner__icon       { flex: none; margin-top: 1px; }

/* A 3px left bar is quoted content (a prompt example, a learner's own words, a
   finding , and never a status. The two widths are the whole distinction. */
.quote              { border-left: 3px solid var(--quote); padding: 13px 16px;
                      background: var(--surface-sunken);
                      font-size: var(--size-body); line-height: 1.65;
                      color: var(--text); text-wrap: pretty; }

/* ======================================================================== *
   Blocks added for the full template set. Same rules: every colour, size and
   family is a token; padding and gap are the stated exception.
   ======================================================================== */

/* ---- Tenant header, the one the shell draws ---------------------------- */

.masthead           { display: flex; align-items: stretch; padding: 0 24px;
                      background: var(--t-raw);
                      border-bottom: 1px solid var(--t-edge); min-height: 56px; }
.masthead__brand    { display: flex; align-items: center; gap: 13px; }
.masthead__plate    { height: 28px; background: var(--surface);
                      border: 1px solid rgba(19, 25, 32, 0.16); padding: 0 9px;
                      display: flex; align-items: center; }
     /* a white plate, because a tenant sending a dark wordmark for a letterhead
        would otherwise vanish into a dark bar */
.masthead__tenant   { font-size: var(--size-body-sm); font-weight: 600;
                      color: var(--t-on); }
.masthead__nav      { margin-left: auto; display: flex; align-items: stretch;
                      gap: 24px; }
/* FULL OPACITY, and the weight carries the distinction.
 *
 * --t-on is ALREADY the derived foreground: it was chosen by luminance to clear
 * 4.5:1 on this fill, and mixing it with transparent re-derives it. On a fill
 * where white clears by a hair (#3A72C8 at 4.75) the 84% mix reads 3.85, and
 * ink's mix reads worse, so there is no percentage that is safe on every tenant
 * colour a customer may send.
 *
 * A derived token must never be re-derived downstream. The active state was
 * already weight and an underline rather than colour, for the greyscale board
 * pack, so the inactive one has a distinction available that costs no contrast:
 * 400 against 600 plus a rule.                                             */
.masthead__link     { display: flex; align-items: center; font-size: var(--size-body-sm);
                      font-weight: 400; color: var(--t-on); text-decoration: none; }
.masthead__link[aria-current="page"] { font-weight: 600; color: var(--t-on);
                      box-shadow: inset 0 -2px 0 var(--t-on); }
     /* weight and an underline, not colour alone: the greyscale board pack */
/* A mix IS safe here and on the avatar ring: both are structure rather than text,
   and --line's own 1.29:1 is the precedent. The rule is about TEXT carrying a
   derived foreground, not about every use of the token. */
.masthead__divider  { width: 1px; align-self: center; height: 22px;
                      background: color-mix(in srgb, var(--t-on) 16%, transparent); }
/* position:relative is what the panel anchors to. Absolute with a margin alone
   put it against the page and ran it off the right edge. */
.masthead__menu     { display: flex; align-items: center; position: relative; }
.masthead__panel    { position: absolute; right: 0; top: 100%; margin-top: 8px;
                      background: var(--surface); border: 1px solid var(--line);
                      padding: 8px 0; min-width: 200px; z-index: 10; }

/* The divider is a sibling of the nav and the menu now, so nothing spaces it: the
   24 either side was the nav container's own gap when all three sat inside it.
   Margin rather than a gap on the bar, because the bar's children are ordered and
   wrapped at 360 and a gap there would space the wrapped rows too. */
.masthead__divider  { margin: 0 24px; }

/* WebKit draws its own marker on a summary. The disclosure has a chevron of its
   own, and two is one too many. */
.masthead__account::-webkit-details-marker { display: none; }
.masthead__account  { cursor: pointer; }

/* The user's name at 360. The tenant plate is already truncating at that width and
   two truncating strings on one row is a row of ellipses, so the avatar carries
   the account and the name goes. */
.masthead__name     { display: inline; }
.masthead__account  { display: flex; align-items: center; gap: 9px; list-style: none;
                      font-size: var(--size-body-sm);
                      color: var(--t-on);
                      background: none; border: none; cursor: pointer; }
.avatar             { width: 27px; height: 27px; border-radius: 50%;
                      border: 1px solid color-mix(in srgb, var(--t-on) 42%, transparent);
                      display: grid; place-items: center; padding-top: 0.43em;
                      box-sizing: border-box; font-family: var(--font-mono);
                      font-size: var(--size-caps); font-weight: 600;
                      color: var(--t-on); }
     /* padding-top because capitals have no descender and place-items centres the
        line box, which sits the initials 2.2px high */
.avatar--ink        { border-color: var(--unrecorded); color: var(--text-secondary); }

@media (max-width: 700px) {
  /* TWO ROWS, and the account menu stays on the FIRST one beside the brand.

     Not in the nav row: that row scrolls sideways at this width, and the account
     disclosure is the one control somebody needs in order to leave. Putting the
     way out on the axis that can carry it off screen is worse than the asymmetry
     it costs.

     Done by WRAPPING AND ORDERING one element, not by duplicating it: a details
     that changes parent loses its open state, and two copies is two things to
     keep in step. DOM order stays lockup, nav, menu at both widths.

     Identity and exit on top, sections beneath, every target at least 44px. */
  .masthead         { flex-wrap: wrap; padding: 0; align-items: center; }
  .masthead__lockup { order: 1; padding: 10px 0 10px 16px; min-width: 0; }
  .masthead__menu   { order: 2; margin-left: auto; padding-right: 16px; }
  .masthead__account { min-height: 44px; }
  .masthead__divider { display: none; }
  .masthead__name   { display: none; }
  /* The gutter below 700 belongs to the brand element, not the lockup: 8e's plate
     is a plain span rather than a link, because a gate's plate may not be a door,
     and it needs the same 16 either way. */
  .masthead__brand  { padding: 10px 0 10px 16px; min-width: 0; }
  .masthead__lockup { padding: 0; min-width: 0; }
  .masthead__nav    { order: 3; flex: 0 0 100%; margin-left: 0; gap: 0;
                      overflow-x: auto;
                      border-top: 1px solid color-mix(in srgb, var(--t-on) 16%, transparent); }
  .masthead__link   { min-height: 44px; padding: 0 16px; }
}



/* ---- Signed-out card -------------------------------------------------- */

.gate               { padding: 40px 24px 44px; display: flex; justify-content: center; }
.gate__card         { width: 400px; max-width: 100%; background: var(--surface);
                      border: 1px solid var(--line); padding: 30px; }
.gate__title        { margin: 13px 0 0; font-size: var(--size-h2); font-weight: 600;
                      letter-spacing: -0.018em; }
.gate__text         { margin: 10px 0 0; font-size: var(--size-body-sm);
                      line-height: 1.6; color: var(--text-secondary); }
.gate__text--ink    { color: var(--text); }
.gate__foot         { margin-top: 20px; border-top: 1px solid var(--line-hairline);
                      padding-top: 14px; display: flex; flex-direction: column;
                      gap: 8px; }
     /* every signed-out card carries the lockup and the anti-phishing line: this is
        the highest-value lookalike surface in the product */

/* ---- Field ------------------------------------------------------------- */

.field              { margin-top: 18px; display: flex; flex-direction: column; gap: 7px; }
.field__label       { font-size: var(--size-body-sm); font-weight: 600; }
.field__label--optional { color: var(--text-secondary); font-weight: 600; }
.field__input       { border: 1.5px solid var(--text); padding: 13px;
                      font-size: var(--size-body); font-family: inherit;
                      background: var(--surface); color: var(--text);
                      border-radius: var(--radius); }
.field__input--mono { font-family: var(--font-mono); }
.field__input--code { font-family: var(--font-mono); font-size: var(--size-h3);
                      letter-spacing: .3em; }
.field__input[aria-invalid="true"] { border-color: var(--warning-line); }
.field__help        { font-size: var(--size-label); line-height: 1.55;
                      color: var(--text-secondary); }
/* A refusal sits where the help text would, in the error ink: it replaces the
   guidance rather than joining it, because guidance somebody has already failed
   to follow is not what they need next. */
.field__error       { font-size: var(--size-label); line-height: 1.55;
                      color: var(--warning-line); }
.field__textarea    { min-height: 104px; }

/* ---- Table, the admin density ----------------------------------------- */

.table__head        { display: grid; border-bottom: 1px solid var(--line); }
.table__th          { padding: 11px 20px; font-family: var(--font-mono);
                      font-size: var(--size-micro); font-weight: 600;
                      letter-spacing: .14em; color: var(--text-secondary);
                      text-align: left; }
.table__row         { display: grid; border-bottom: 1px solid var(--line-hairline);
                      align-items: center; }
.table__cell        { padding: 13px 20px; font-size: var(--size-body-sm); }
.table__cell--meta  { font-size: var(--size-label); color: var(--text-secondary); }
.table__cell--num   { font-variant-numeric: tabular-nums; }
.table--people      { grid-template-columns: minmax(0, 1fr) 190px 150px 130px; }
.table--roles       { grid-template-columns: minmax(0, 1fr) 268px 168px; }
.table--mapping     { grid-template-columns: 250px 250px minmax(0, 1fr); }
.table--groups      { grid-template-columns: 96px minmax(0, 1fr) 200px; }

@media (max-width: 700px) {
  .table--people, .table--roles, .table--mapping, .table--groups {
    grid-template-columns: minmax(0, 1fr); }
  .table__head      { display: none; }
  .table__row       { padding: 14px 0; gap: 6px; }
  .table__cell      { padding: 0 16px; }
}

/* In flight: the previous result stays and goes quiet. Not skeleton rows, which
   would remove the only thing telling somebody their search was right. */
.table__body--pending .table__row { opacity: 0.55; }

/* ---- Toolbar: search first, then filters ------------------------------ */

.toolbar            { padding: 18px 20px; border-bottom: 1px solid var(--line);
                      display: flex; flex-direction: column; gap: var(--s-4); }
.toolbar__search    { flex: 1; min-width: 300px; display: flex; align-items: center;
                      gap: 10px; border: 1.5px solid var(--text); padding: 11px 13px; }
.toolbar__row       { display: flex; align-items: stretch; gap: 10px; flex-wrap: wrap; }
.toolbar__bulk      { border-top: 1px solid var(--line-hairline); padding-top: 14px;
                      display: flex; flex-direction: column; gap: 10px; }
     /* the action row and the sentence explaining its scope are separate rows: a
        sentence squeezed beside a button wraps into a ragged column */

/* ---- Paging: numbered links, real URLs -------------------------------- */

.paging             { padding: 14px 20px; display: flex; align-items: center;
                      gap: var(--s-4); flex-wrap: wrap; }
.paging__list       { margin-left: auto; display: flex; align-items: center; gap: 6px; }
/* line-height: 1, same rule as .btn: a paging link is a button by another name and
   its height is padding plus type. .masthead__link and .field__input are excepted
   deliberately: the first is a flex item filling a 56px bar, and the second holds a
   line somebody is typing, which needs room to be read. */
.paging__link       { padding: 7px 12px; border: 1px solid var(--unrecorded);
                      font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 600; line-height: 1; color: var(--text);
                      text-decoration: none; }
.paging__link[aria-current="page"] { border: 1.5px solid var(--text); }
.paging__link[aria-disabled="true"] { border-color: var(--line); color: var(--inert);
                      pointer-events: none; }
     /* numbered, because ?page=2 is a real URL, a row stays linkable and the back
        button works. Infinite scroll has no no-JS fallback. */

/* ---- Stepper ----------------------------------------------------------- */

.stepper            { display: flex; align-items: center; gap: var(--s-3);
                      flex-wrap: wrap; }
.stepper__step      { font-family: var(--font-mono); font-size: var(--size-caps);
                      font-weight: 600; color: var(--text-secondary); }
.stepper__step--now { color: var(--text); box-shadow: inset 0 -2px 0 var(--text);
                      padding-bottom: 3px; }
.stepper__rule      { width: 20px; height: 1px; background: var(--rule); }

/* ---- Figure list: a count that leads --------------------------------- */

.figure__row        { padding: 17px 0; border-top: 1px solid var(--line-hairline);
                      display: flex; gap: var(--s-5); align-items: baseline; }
.figure__count      { flex: none; width: 72px; font-size: 30px; font-weight: 600;
                      letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
                      line-height: 1; }
     /* counts, never percentages: the levels are ordinal and a proportion implies
        a precision the data has not got */
.figure__name       { font-size: var(--size-body); font-weight: 600;
                      letter-spacing: -0.01em; }
.figure__line       { margin-top: 4px; font-size: var(--size-body-sm);
                      line-height: 1.5; color: var(--text-secondary); }

/* ---- Progress: a step finished. Never ink ---------------------------- */

.progress           { display: flex; gap: 3px; }
     /* No background. tokens.css declared a .progress swatch that painted one, so
        the grey showed through wherever the blocks did not fill the row; that class
        is .swatch-progress now. A container paints nothing: the blocks carry every
        fill, which is what lets a row be read as done against todo. */
.progress__block    { flex: none; box-sizing: border-box; }
.progress__block--done { background: var(--progress); border: 1px solid var(--progress); }
.progress__block--todo { background: var(--surface);
                      border: 1.5px dotted var(--unrecorded); }
     /* progress is not evidence. A finished lesson is a checked box, never an ink
        fill, and that distinction is what separates this from an LMS. */

/* ---- Lesson rail ------------------------------------------------------- */

.lesson             { display: flex; align-items: flex-start; gap: 10px;
                      padding: 10px 0; border-top: 1px solid var(--line-hairline);
                      text-decoration: none; color: var(--text); }
.lesson__title      { font-size: var(--size-body-sm); line-height: 1.35; }
.lesson__title--done { color: var(--text-secondary); }
.lesson__meta       { margin-top: 4px; display: flex; align-items: center; gap: 7px;
                      font-size: var(--size-label); color: var(--text-secondary); }
.lesson--locked     { pointer-events: none; }

/* ---- Reading measure --------------------------------------------------- */

.prose              { max-width: 68ch; font-size: var(--size-body-lg);
                      line-height: 1.75; color: var(--text); text-wrap: pretty; }
     /* 17px and never smaller: somebody is in this for forty-five minutes */
.prose h3           { margin: 32px 0 0; font-size: var(--size-h3); font-weight: 600;
                      letter-spacing: -0.015em; }
.prose p            { margin: 14px 0 0; }

/* ---- Definition rows: a record's facts -------------------------------- */

.facts__row         { padding: 14px 0; border-top: 1px solid var(--line-hairline);
                      display: grid; grid-template-columns: 210px minmax(0, 1fr);
                      gap: var(--s-5); align-items: baseline; }
.facts__term        { font-size: var(--size-body-sm); line-height: 1.45;
                      color: var(--text-secondary); }
.facts__value       { font-size: var(--size-body-sm); line-height: 1.45;
                      color: var(--text); }
.facts__note        { margin-top: 5px; max-width: 64ch; font-size: var(--size-label);
                      line-height: 1.55; color: var(--text-secondary); }

@media (max-width: 700px) {
  .facts__row       { grid-template-columns: minmax(0, 1fr); gap: 4px; }
}

/* ---- Mark: the semantic glyph beside a claim -------------------------- */

.mark               { flex: none; width: 26px; height: 26px; box-sizing: border-box;
                      display: grid; place-items: center; }
.mark--at           { border: 1.5px solid var(--evidence); }
.mark--below        { background: var(--gap-fill); border: 1px solid var(--gap-line);
                      background-image: repeating-linear-gradient(45deg,
                        var(--gap-line) 0 1.5px, transparent 1.5px 5px); }
.mark--none         { background: var(--surface);
                      border: 1.5px dotted var(--unrecorded); }

/* ---- Empty state ------------------------------------------------------- */

.empty              { padding: 40px 28px; max-width: 62ch; }
.empty__title       { margin: 0; font-size: var(--size-h2); font-weight: 600;
                      letter-spacing: -0.018em; }
.empty__text        { margin: 11px 0 0; font-size: var(--size-body);
                      line-height: 1.65; color: var(--text-secondary);
                      text-wrap: pretty; }
     /* an empty list explains its own emptiness. It never offers work to fill the
        space, and it never congratulates. */

/* ---- Dropzone: the file control on import step 1 ---------------------- *
 * A dashed boundary rather than a solid card edge, because it is a target
 * and not a surface. The dashes say so without needing a colour.
 *
 * The visible control is a <label for> wrapping nothing and the real input
 * is offscreen, NOT display:none: a hidden-by-clip input keeps keyboard
 * focus and the form works with the attribute ignored. The label carries
 * .btn .btn--secondary, so it takes the same hover and focus ring as every
 * other secondary control.                                              */

.dropzone           { margin-top: var(--s-5); border: 1.5px dashed var(--unrecorded);
                      padding: 28px 24px; display: flex; flex-direction: column;
                      align-items: flex-start; gap: var(--s-3); }
.dropzone__prompt   { font-size: var(--size-body-sm); color: var(--text); }
.dropzone__note     { font-size: var(--size-meta); color: var(--text-secondary); }
.dropzone__input    { position: absolute; width: 1px; height: 1px; padding: 0;
                      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
                      border: 0; }
.dropzone__input:focus-visible + .dropzone__note { outline: 2px solid var(--text);
                      outline-offset: 2px; }
     /* the input is clipped, so its own ring would be invisible: the label is
        what a keyboard user tabs to and it rings normally */

/* ---- Checklist: a rail list read against something the user already has */

.checklist          { margin: 12px 0 0; padding: 0; list-style: none;
                      display: flex; flex-direction: column; gap: var(--s-3); }
.checklist__item    { display: flex; align-items: flex-start; gap: 10px;
                      font-size: var(--size-body-sm); line-height: 1.55;
                      color: var(--text); }
.checklist__box     { flex: none; width: 14px; height: 14px; margin-top: 3px;
                      box-sizing: border-box; border: 1.5px solid var(--unrecorded); }
     /* an empty square, not a tick: nothing has been satisfied yet, and a tick
        here would claim the file already meets the requirement */

/* ---- Split: a wider main column than the standard rail --------------- *
 * The dashboard's shortfall rows carry a rank, a large count and two lines
 * of text. At 1fr / 300px the count and the name drift far enough apart
 * that the eye loses the pairing, so the main column takes 1.6fr and the
 * second column is a card in its own right rather than a margin.        */

.layout--split      { display: grid;
                      grid-template-columns: minmax(420px, 1.6fr) minmax(300px, 1fr);
                      gap: var(--s-5); align-items: stretch; }

@media (max-width: 900px) {
  .layout--split    { grid-template-columns: minmax(0, 1fr); }
}

/* A card whose action sits at the bottom whatever the rows above it are,
   so two cards side by side end level. */
.card--actionfoot   { display: flex; flex-direction: column; }
.card--actionfoot .card__body { flex: 1; }
.card__actionfoot   { padding: 0 20px 20px; }

/* ---- Figure row: a count that leads --------------------------------- */

.figure__rank       { flex: none; width: 22px; font-family: var(--font-mono);
                      font-size: var(--size-label); font-weight: 600;
                      color: var(--text-secondary); }
.figure__row--small { padding: 12px 0; gap: 14px; }
.figure__count--small { width: 44px; font-size: var(--size-h3); }

/* ---- Matrix: coverage by department ---------------------------------- *
 * Counts against a denominator, NEVER a percentage: the levels are ordinal
 * and a proportion implies a precision the data has not got.
 *
 * EVERY CELL CARRIES A GLYPH as well as a fill. This is the surface that
 * goes into a black-and-white board pack, so the fill is reinforcement and
 * the glyph is the encoding:
 *     filled circle   at or above requirement
 *     half circle     below requirement
 *     open circle     no one at requirement
 *     middle dot      not yet assessed   (never a zero)
 *
 * The row head is 230px and fixed; the department columns share what is
 * left. A 1fr row head would let a long competency name push the counts
 * out of alignment between rows.                                        */

.matrix             { display: flex; flex-direction: column; }
.matrix__row        { display: grid; grid-template-columns: 230px repeat(5, 1fr);
                      border-top: 1px solid var(--line-hairline); }
.matrix__row--head  { border-top: none; }
.matrix__corner     { padding: 8px 20px; }
.matrix__th         { padding: 8px 6px; text-align: center;
                      font-family: var(--font-mono); font-size: var(--size-micro);
                      font-weight: 600; letter-spacing: .1em;
                      color: var(--text-secondary); }
.matrix__rowhead    { padding: 10px 20px; display: flex; align-items: baseline;
                      gap: 8px; min-width: 0; }
.matrix__rowname    { font-size: var(--size-meta); line-height: 1.35; }
.matrix__cell       { padding: 10px 6px; display: flex; align-items: center;
                      justify-content: center; gap: 6px;
                      font-family: var(--font-mono); font-size: var(--size-label);
                      font-variant-numeric: tabular-nums; }
.matrix__glyph      { font-size: var(--size-meta); line-height: 1; }
.matrix__cell--at        { color: var(--text); }
.matrix__cell--below     { color: var(--gap-text); background: var(--gap-fill); }
.matrix__cell--none      { color: var(--gap-text); background: var(--gap-fill); }
.matrix__cell--unassessed { color: var(--inert); }
.matrix__legend     { padding: 12px 20px 18px; display: flex; flex-wrap: wrap;
                      gap: var(--s-4); font-size: var(--size-label);
                      color: var(--text-secondary); }

@media (max-width: 900px) {
  /* The matrix scrolls sideways rather than reflowing: a competency row read
     across its departments is the whole point of the component, and stacking
     it into five separate lists destroys the comparison. */
  .matrix           { overflow-x: auto; }
  .matrix__row      { grid-template-columns: 160px repeat(5, minmax(76px, 1fr));
                      min-width: 540px; }
}

/* A wider measure than .measure, for a column of cards rather than prose.
   760 is the reading width; this is the day-one dashboard, which is one
   decision plus two supporting cards and wants a little more room. */
.measure-wide       { max-width: 860px; }

/* ---- A narrower rail, for the assessment ----------------------------- *
 * 260 rather than 300. The assessment rail lists section names and nothing
 * else, and at 300 it starts competing with the question for attention
 * during the one task on this product where attention is the whole point. */

.layout--rail-narrow { display: grid;
                      grid-template-columns: minmax(0, 1fr) 260px;
                      gap: var(--s-6); align-items: start; }

@media (max-width: 900px) {
  .layout--rail-narrow { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Numbered steps: what happens next ------------------------------- *
 * The marker is ink where the learner is NOW and dotted for what follows.
 * Ink here is progress, not evidence: it says this is where you are, and
 * day one is the only learner surface where ink appears before a baseline
 * has certified anything.                                                */

.step__marker       { flex: none; width: 24px; height: 24px; display: grid;
                      place-items: center; padding-top: 0.43em;
                      box-sizing: border-box; font-family: var(--font-mono);
                      font-size: var(--size-caps); font-weight: 600;
                      border: 1.5px dotted var(--unrecorded);
                      color: var(--text-secondary); }
.step__marker--now  { border: 1px solid var(--evidence); background: var(--evidence);
                      color: var(--surface); }
.step__title        { font-size: var(--size-body-sm); font-weight: 600;
                      line-height: 1.3; }
.step__title--later { color: var(--text-secondary); }
.step__note         { margin-top: 5px; font-size: var(--size-meta);
                      line-height: 1.55; color: var(--text-secondary); }

/* ---- Stats strip: facts the paragraph above does not carry ----------- *
 * Four at desktop, two at 720, two at 360. NOT tiles restating the
 * sentence above them: an earlier version had two of four repeating the
 * paragraph verbatim about 80px below it.                               */

.stats              { margin-top: var(--s-5); display: grid;
                      grid-template-columns: repeat(4, 1fr);
                      border-top: 1px solid var(--line-hairline); }
.stats__item        { padding: 14px 0 0; }
.stats__label       { font-family: var(--font-mono); font-size: var(--size-micro);
                      font-weight: 600; letter-spacing: .14em;
                      color: var(--text-secondary); }
.stats__value       { margin-top: 5px; font-size: var(--size-h3); font-weight: 600;
                      letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }

@media (max-width: 720px) { .stats { grid-template-columns: repeat(2, 1fr);
                      gap: 0 var(--s-4); } }

/* ---- The matrix at narrow widths -------------------------------------- *
 * It does not survive 360px, so it does not go there. The words and the
 * outstanding setup do, and the grid becomes a link. At 720 it loses two
 * department columns rather than compressing five.                       */

.matrix-narrow      { display: none; }

@media (max-width: 720px) {
  .matrix-card .matrix__row { grid-template-columns: 200px repeat(3, 1fr); }
  .matrix-card .matrix__th:nth-child(n+5),
  .matrix-card .matrix__cell:nth-child(n+5) { display: none; }
}

@media (max-width: 560px) {
  .matrix-card      { display: none; }
  .matrix-narrow    { display: block; }
}

/* The sentence naming the columns the 720 breakpoint hides. It exists only
   at that width: above it nothing is hidden, below it the whole grid has
   gone and the card says so instead. A column that vanishes silently is
   indistinguishable from a department that does not exist. */
.matrix__hidden     { display: none; }

@media (max-width: 720px) and (min-width: 561px) {
  .matrix__hidden   { display: inline; }
}

/* A greeting, above the headline and quieter than it. Not a headline of its
   own: it is the only line on a learner surface addressed to the person
   rather than about their record, and giving it heading weight would put a
   pleasantry above the thing they came for. Omitted entirely when no name
   is held , an invited learner who has not activated may have only an
   address, and "Hi there" is worse than nothing. */
.greeting           { font-size: var(--size-body-lg); line-height: 1.4;
                      color: var(--text-secondary); margin-bottom: 8px; }

/* A stamp: a fact about the field it sits beside, stated where the field is
   rather than in help text below it. NEVER SCORED is the one thing a learner
   needs before they start typing, and help text after the box is read after
   the decision it was meant to inform. Outlined rather than filled, because
   it is a statement and not a status. */
.stamp              { font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 600; line-height: 1; letter-spacing: .12em; padding: 3px 8px;
                      border: 1px solid var(--text); color: var(--text);
                      white-space: nowrap; }
     /* Ink, not muted. NEVER SCORED is the one fact somebody needs BEFORE they
        type, and setting it in the quietest colour on the surface puts it last
        in the reading order of the thing it governs. */

/* ---- Fixbox: the exact path out of a refusal ------------------------- *
 * Somebody who has just been told we cannot read their file is in Excel
 * now and wants the menu item, not an explanation. The path is set in the
 * mono face so it reads as something to find rather than something to
 * understand, and it sits between the cause and the reassurance.        */

.fixbox             { margin-top: var(--s-3); padding: 12px 16px;
                      border: 1px solid var(--line); }
.fixbox__path       { margin-top: 6px; font-family: var(--font-mono);
                      font-size: var(--size-body-sm); color: var(--text); }

/* One person's figures, sentence-cased. NOT .stats, whose mono caps belong to a
   strip of organisation statistics: three facts about somebody are not the same
   object as ROLES SET and PEOPLE ACTIVE, and using one treatment for both makes a
   person read as a metric. */
.person__figure     { margin-top: 5px; font-size: var(--size-h3); font-weight: 600;
                      letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }

/* ---- Learning status: whether the work is moving --------------------- *
 * Per department, because an aggregate hides the department that has done
 * nothing, which is the only row worth acting on. Three counts and a
 * total, never a percentage.                                            */

.learning__row      { display: grid; grid-template-columns: 230px repeat(3, 1fr) 150px;
                      border-top: 1px solid var(--line-hairline);
                      align-items: center; }
.learning__row--head { border-top: 1px solid var(--line); }
.learning__count    { padding: 11px 6px; text-align: center;
                      font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 600; font-variant-numeric: tabular-nums;
                      color: var(--text); }
.learning__total    { flex: none; font-family: var(--font-mono);
                      font-size: var(--size-label); font-weight: 600;
                      font-variant-numeric: tabular-nums;
                      color: var(--text-secondary); }

@media (max-width: 900px) {
  .learning__row    { grid-template-columns: 160px repeat(3, 1fr) 110px;
                      min-width: 560px; }
}

/* ---- Choice: pick the fact, do not write a message ------------------- *
 * Each option is a field an administrator can act on. A text box here
 * produces a paragraph arriving somewhere nobody owns, and invites a
 * learner to type something about themselves into a box nobody told them
 * was a record.                                                         */

.choice             { display: flex; gap: var(--s-3); align-items: flex-start;
                      border: 1px solid var(--unrecorded); padding: 13px 15px;
                      cursor: pointer; }
.choice:hover       { border-color: var(--text); background: var(--line-hairline); }
.choice__title      { display: block; font-size: var(--size-body-sm);
                      font-weight: 600; line-height: 1.4; }
.choice__note       { display: block; margin-top: 4px; font-size: var(--size-meta);
                      line-height: 1.5; color: var(--text-secondary); }

/* The container .learning__row's 560px minimum needs. Without it the row overflows
   its card silently, which is what the matrix has .matrix-narrow for. */
.learning__scroll   { overflow-x: auto; }

/* A column head that is a door. It keeps the head's type and takes the link colour,
   because it is a destination and the heads beside it are not: "No department" has no
   page, so its head is plain text and the difference has to be visible. */
.matrix__th-link    { color: var(--t-fg); text-decoration: none; }
.matrix__th-link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ---- The tenant lockup: ONE link over the plate and the name --------- *
 * A tenant that sends a logo has it in the plate and its name beside it,
 * so a cue on the plate's own contents has nothing to attach to: the logo
 * is the tenant's file and we do not restyle it. The NAME is text, it is
 * present whether or not a logo is, and it is inside the same anchor, so
 * the underline lands on it in both states. One rule, one treatment, and
 * no second hover for the logo case.
 *
 * The focus ring is --t-on, the luminance-derived token the bar's own text
 * already uses: white on a navy fill at 13.8:1, ink on a pale gold at
 * 7.31:1. A white ring would be 2.4:1 on that gold, under the 3:1 a focus
 * indicator has to clear.
 *
 * NOT an outer box-shadow. "Nothing moves on hover" forbids it, and an
 * outer halo on a white plate was the one hover that could not be an
 * inset wash, which is what made it the wrong answer rather than the
 * exception it looked like.
 *
 * These rules live HERE now. They were in the product's base.css because
 * this file declared no plate state, and a control state carried by the
 * build rather than by the stylesheet is a state nothing holds to the
 * token set.                                                            */

.masthead__lockup       { display: flex; align-items: center; gap: 13px;
                          text-decoration: none; min-width: 0; }

/* The plate holds the tenant's LOGO where they sent one and their NAME where they
   did not. It is never both: a tenant has one name and no wordmark, so a plate that
   printed the name beside a .masthead__tenant carrying the same name showed it
   twice in a row.

   The ink is declared, not inherited. The lockup is an anchor, so the link colour
   and a:hover would otherwise paint a tenant's name in the tenant's own colour on
   top of the tenant's own fill.

   And it truncates. A 53-character name with no logo ran the plate to 364px and
   overflowed the bar at 360. Where there IS a logo the name sits outside the plate
   and wraps instead, because the plate is then a fixed-size image and the text has
   somewhere to go. */
.masthead__plate--name  { font-size: var(--size-label); font-weight: 600;
                          color: var(--text); text-decoration: none;
                          white-space: nowrap; overflow: hidden;
                          text-overflow: ellipsis; max-width: 220px; }
.masthead__lockup:hover .masthead__plate--name { text-decoration: underline;
                          text-underline-offset: 2px; }

@media (max-width: 700px) { .masthead__plate--name { max-width: 180px; } }
.masthead__lockup:hover .masthead__tenant { text-decoration: underline;
                          text-underline-offset: 2px; }
.masthead__lockup:focus-visible { outline: 2px solid var(--t-on);
                          outline-offset: 2px; }

/* 8e is the one signed-in surface whose plate is NOT a door: confirm-your-
   profile gates progress, so the lockup is a plain span there and takes
   neither state. */

/* ---- Scope band: a filter set from another page --------------------- *
 * The shortfall filter arrives from the department page as ONE scope, two
 * facts together, so it is not a third dropdown beside state and
 * department: a competency with no department chosen is a list nobody
 * asked for, and the scope arrives whole or not at all.
 *
 * It states what the list is showing and how to leave it, and both are
 * load-bearing. The sentence is the reason the list looks short, and
 * without the clear an administrator is stuck in a scope they did not
 * choose. At 360 it stacks and goes FIRST: the sentence cannot be the
 * thing that scrolls away.                                             */

.scope              { background: var(--surface-sunken);
                      border: 1px solid var(--line); padding: 14px 18px;
                      display: flex; align-items: center; gap: 18px;
                      flex-wrap: wrap; }
.scope__text        { flex: 1; min-width: 0; display: flex;
                      flex-direction: column; gap: 5px; }
.scope__sentence    { font-size: var(--size-body-sm); line-height: 1.55;
                      color: var(--text); }

@media (max-width: 700px) {
  .scope            { flex-direction: column; align-items: stretch;
                      padding: 13px 15px; gap: 11px; }
}

/* The scoped list's columns. DEPARTMENT and STATE go: every row is in the
   same department by definition, and an invitation state does not explain
   why somebody is in a shortfall list. THEIR LEVEL and THEIR ROLE NEEDS
   take their place, because a list that asserts a shortfall has to show
   it , and the level alone would not, since the same Level 2 is a
   shortfall for one role and enough for another. */
.table--shortfall   { grid-template-columns: minmax(0, 1fr) 200px 150px 150px; }

@media (max-width: 700px) {
  /* Cards, not a sideways table: four columns at 360 would put the two
     levels off-screen, and those are the columns this list exists for. */
  .table--shortfall { grid-template-columns: minmax(0, 1fr); gap: 7px;
                      padding: 14px 0; }
}

/* ---- The shortfall row's required cell -------------------------------- *
 * The word is visible at 360 and hidden at desktop, and it is not extra
 * copy: at desktop the fourth column is headed REQUIRED and the cell holds
 * "3 Advanced". At 360 the columns collapse and the heads go, so the word
 * carries what the head was carrying.
 *
 * It cannot be dropped. A list asserting a shortfall has to show the level
 * somebody has and the level their role requires, and without the word
 * that is two bare numbers with nothing saying which is which: the one
 * ambiguity this list cannot afford.
 *
 * These rules were in the product's base.css because this file had none
 * for the cell. Same reason as the plate's two states: a control state
 * carried by the build rather than the stylesheet is a state nothing
 * holds to the token set.                                               */

.shortfall__needs       { display: none; }

@media (max-width: 700px) {
  .shortfall__needs     { display: inline; font-size: var(--size-meta);
                          color: var(--text-secondary); }
  /* the chip and the word sit on one line at 360, in meta type, with no
     subline under the count: the head they replace was one line too */
  .table--shortfall .table__cell--required { display: flex; align-items: baseline;
                          gap: 8px; flex-wrap: wrap; }
}

/* The subline under a shortfall count goes at 360. The count and the chip
   carry the fact there; a third line under them restates the head that has
   already been removed, and at that width every line costs a row. */
.shortfall__sub     { font-size: var(--size-meta); color: var(--text-secondary); }
@media (max-width: 700px) { .shortfall__sub { display: none; } }

/* ---- The chip has FOUR states, not three ----------------------------- *
 * derive() returns at, above, below and unassessed, and a chip naming
 * three collapsed above into at. They are both "not short", but printing
 * "at level" for somebody who exceeded their requirement understates a
 * record the evidence page will show in full, so above says its own word
 * and shares at's border: the question a chip answers is whether this
 * person is short, and the answer is no in both.                        */
.chip--above        { border: 1px solid var(--evidence); color: var(--evidence); }

/* The baseline result's rows: name and start label, not the eight-step track,
   so the level column is 150 rather than the record's 262. At 700 it becomes
   one column and the label moves under the name: a fixed level column at 360
   starves the name it is explaining, leaving about 154px for a name like
   Human Oversight & Escalation. */
.record__row--baseline { padding: 13px 28px;
                      grid-template-columns: minmax(0, 1fr) 150px; }
@media (max-width: 700px) {
  .record__row--baseline { grid-template-columns: minmax(0, 1fr); gap: 4px;
                      padding: 13px 18px; }
  .card--sectioned .card__head { padding: 20px 18px; }
  .card--sectioned .card__foot { padding: 16px 18px 20px; }
}

/* A correction row: the file's value beside a field for what it should be.
   Three columns at desktop; at 700 the original sits above the field, which
   keeps the reading order (what it says, then what you type) rather than
   putting a bare field first. */
.fixrow             { display: grid; grid-template-columns: 48px minmax(0,1fr) minmax(0,1fr);
                      gap: var(--s-4); padding: 11px 0;
                      border-top: 1px solid var(--line-hairline); align-items: center; }
@media (max-width: 700px) {
  .fixrow           { grid-template-columns: 48px minmax(0,1fr); row-gap: 8px; }
  .fixrow > :last-child { grid-column: 2; }
}

/* ---- The unplaced-titles table --------------------------------------- *
 * Its own columns, because an inline grid-template-columns on the head and
 * the rows beats the 700px collapse and the role select ended up over the
 * title at 360. Same reason .table--groups and .table--shortfall carry
 * theirs: a column set declared in markup is a column set the breakpoint
 * cannot reach.
 *
 * At 700 it becomes one column. The select is the widest thing in the row
 * and a three-column grid at that width gives it about 90px, which is not
 * enough for a role name.                                               */

.table--titles      { grid-template-columns: 76px minmax(0, 1fr) 210px; }

@media (max-width: 700px) {
  .table--titles    { grid-template-columns: 76px minmax(0, 1fr); row-gap: 8px; }
  .table--titles > :last-child { grid-column: 2; justify-content: flex-start; }
}

/* ---- The import job's block row -------------------------------------- *
 * The blocks DIVIDE the width they are given, which is the board's own
 * sentence: twenty-four at a written 22px is 528 plus gaps and overflows
 * 360 by two hundred. flex:1 with a minimum is the only form that holds at
 * both widths, and min-width is what stops twenty-four of them becoming
 * hairlines on a phone.
 *
 * Wrapping rather than shrinking below that: two rows of twelve still read
 * as a quantity, where twenty-four 4px slivers read as a texture.        */

.progress--wide     { flex-wrap: wrap; row-gap: 3px; }
.progress--wide .progress__block { flex: 1 1 18px; min-width: 18px; height: 10px; }

/* ---- Activity: the tenant's own record of what was done -------------- *
 * Four columns, because four facts answer the question the page is opened
 * with: what happened, when, who did it, and to whom.
 *
 * At 700 it becomes a block per change and the DATE MOVES LAST, where it
 * sits second at desktop: a block is read downward and a row is scanned
 * across, so the date is a column somebody's eye lands on and a line
 * somebody reads after the sentence.                                    */

.table--activity    { grid-template-columns: minmax(0,1.5fr) 160px minmax(0,1fr)
                      minmax(0,1.2fr); }
.activity__action   { font-family: var(--font-mono); font-size: var(--size-meta);
                      line-height: 1.4; color: var(--text); }
.activity__detail   { margin-top: 4px; font-size: var(--size-label);
                      line-height: 1.5; color: var(--text-secondary); }
.activity__when     { font-family: var(--font-mono); font-size: var(--size-label);
                      color: var(--text-secondary); font-variant-numeric: tabular-nums; }

/* An erased actor or a vanished target reads in WORDS, and in FULL INK.
 *
 * It was --inert, which is 3.11:1 and which this token set reserves for the label
 * on a control nobody can press: "NEVER ON DATA". Review was right that these
 * words are data (they are the answer to "who did this"), so they take the
 * ordinary text colour and nothing about the row softens in colour.
 *
 * The words already carry the loss. A colour repeating what the words say is the
 * never-colour-alone rule pointed the other way, and it cost contrast to say
 * something twice.
 *
 * What must not change: never an identifier, which is the one thing somebody could
 * copy, paste and get nowhere with, and would be data about an erased person on a
 * page not allowed to hold it. Never blank either, which reads as a change nobody
 * made. */
.activity__lost     { color: var(--text); }

@media (max-width: 700px) {
  .table--activity  { grid-template-columns: minmax(0, 1fr); gap: 5px; }
  .table--activity .table__cell { padding: 0 16px; }
  .activity__when   { order: 3; }
}

/* ---- Departments: teams nest, they are not a second field ------------ *
 * A person's department IS their team's department, so the tree is the
 * interface rather than two fields that can disagree.                   */

.dept               { padding: 16px 24px; border-bottom: 1px solid var(--line-hairline); }
.dept:last-child    { border-bottom: none; }
.dept__head         { display: flex; align-items: baseline; gap: var(--s-4);
                      flex-wrap: wrap; }
.dept__name         { font-size: var(--size-body-lg); font-weight: 600;
                      letter-spacing: -0.012em; }
/* 400, and the boards are redrawn to match. Mono already reads heavier than the
   sans beside it, so 500 makes a secondary count compete with the name it belongs
   to. The stylesheet holds on every one of these four. */
.dept__count        { font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 400; color: var(--text-secondary); }
/* A TEAM's noun goes at 360 and a department's stays. The department row above
   establishes what is being counted, so a team inherits the unit from its parent
   and "22" is unambiguous where the width is worth more. The department keeps it,
   because a count with no noun and nothing above it is a number. */
@media (max-width: 700px) {
  .dept__team .dept__noun { display: none; }
}
.dept__teams        { margin-top: 11px; }
.dept__team         { display: flex; align-items: baseline; gap: var(--s-4);
                      padding: 8px 0 8px 18px;
                      border-top: 1px solid var(--line-hairline); }
.dept__tick         { flex: none; width: 12px; height: 1px; background: var(--rule);
                      position: relative; top: -4px; }
.dept__add          { padding: 9px 0 0 30px;
                      border-top: 1px solid var(--line-hairline); }

.table--units       { grid-template-columns: minmax(0,1fr) 120px 150px; }
/* A suspect name takes the gap INK on no fill: it is a name to look at, not a
   shortfall, and the hatch means a person is below a requirement. */
.unit--suspect      { color: var(--gap-text); font-weight: 600; }

/* ---- The choosers: one shape for the role and the department --------- */

.choose             { padding: 0 26px 8px; display: flex; flex-direction: column;
                      gap: 9px; }
.choose__item       { display: flex; align-items: baseline; gap: var(--s-4);
                      padding: 14px 18px; border: 1px solid var(--unrecorded);
                      text-decoration: none; }
.choose__item:hover { background: var(--line-hairline); border-color: var(--text); }
/* The current choice STAYS in the list and carries the heavier border: a list
   missing the answer somebody expects reads as a list that is wrong. */
.choose__item--current { border: 1.5px solid var(--text); }
/* Nested BEFORE current, and current repeated after it: a team that is the current
   choice must keep the heavier ink edge, and a single-class rule later in the file
   was overriding it. The current state outranks the nesting, always: somebody is
   looking for where they are. */
.choose__item--nested { padding: 11px 16px; border-color: var(--line-strong); }
.choose__item--current,
.choose__item--nested.choose__item--current { border: 1.5px solid var(--text); }
.choose__nest       { display: flex; flex-direction: column; gap: 7px;
                      padding-left: 22px; }
.choose__name       { flex: 1; min-width: 0; font-size: var(--size-body);
                      font-weight: 600; color: var(--text); }
.choose__name--nested { font-size: var(--size-body-sm); font-weight: 400; }
.choose__count      { font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 400; color: var(--text-secondary); }

/* ---- A head that pairs a title with an action ------------------------- *
 * min-width on the title column was 340, which is wider than the 312 a
 * 360px viewport leaves: the intro overflowed and the page scrolled
 * sideways. A min-width is a promise the viewport cannot always keep, so
 * the column is minmax(0,1fr) and the row wraps instead.                */

.page__head         { display: flex; align-items: flex-end; gap: var(--s-5);
                      flex-wrap: wrap; }
.page__head > div   { flex: 1 1 340px; min-width: 0; }

@media (max-width: 700px) {
  .page__head       { align-items: stretch; }
  .page__head > div { flex: 1 1 100%; }
}

/* ---- The confirm block at 360 ----------------------------------------- *
 * It had no narrow rule where every other table has one, so three columns
 * overprinted. A block per unit, and the row count moves under the name
 * with its label, because at this width a bare "1 row" beside a name is
 * the fact that catches a typo and it cannot be the thing that wraps.   */

@media (max-width: 700px) {
  .table--units     { grid-template-columns: minmax(0, 1fr); gap: 4px; }
  .table--units .table__cell { padding: 0 16px; }
  .table--units .table__cell--num::after { content: " in this file"; }
}

/* ---- A select, with our arrow rather than the platform's -------------- *
 * appearance:none and a drawn glyph, because the browser's own arrow is a
 * different shape on every platform and the one thing a control in this
 * product must not be is differently shaped per machine.
 *
 * THE GLYPH IS A BACKGROUND IMAGE, not a sibling span. Three templates
 * shipped a bare <select> and the platform arrow rendered on each: a
 * decoration that must be remembered in markup is a decoration that will
 * be forgotten, and this is the form a template cannot omit.
 *
 * Its text is --text, not --text-secondary. A select holds a VALUE, and a
 * value somebody chose is data: the boards drew it secondary and were
 * wrong, by the same rule that kept --inert off a datum.               */

.select             { appearance: none; -webkit-appearance: none;
                      border: 1px solid var(--unrecorded); border-radius: var(--radius);
                      padding: 11px 34px 11px 13px;
                      font-family: inherit; font-size: var(--size-body-sm);
                      color: var(--text); background-color: var(--surface);
                      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1 L5 5 L9 1' stroke='%234B5663' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
                      background-repeat: no-repeat;
                      background-position: right 13px center; }
.select:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.select--unset      { border-style: dotted; border-width: 1.5px;
                      color: var(--text-secondary); }
     /* a select with nothing chosen yet holds no value, so it may recede */

/* ---- One role's eight levels ----------------------------------------- *
 * Four cells per row rather than a select. The four are ORDINAL and a
 * dropdown hides that: somebody setting eight levels is comparing them
 * down the column, and a closed select shows one value at a time.
 *
 * A radio per cell, clipped rather than display:none, so the row works
 * with the attribute ignored, one choice per competency is enforced by the
 * browser rather than by us, and the cell keeps keyboard focus.
 *
 * Two marks, because a row mid-edit has two facts: the CURRENT level takes
 * the ink border and the PENDING one the ink fill, so it reads as both
 * what it is and what it will be. Where they are the same, only the fill
 * shows , which is the resting state and needs no second mark.          */

.table--levels      { grid-template-columns: 52px minmax(0, 1fr) 224px; }
.levels             { display: flex; gap: 5px; padding: 11px 20px; }
     /* 20 horizontal, NOT 0. This replaces .table__cell's padding rather than adding
        to it, so zeroing the sides put the fourth cell flush against the card's
        border. Same fault as the roles list's marks at handover 25: a cell that
        overrides a row's padding owes the row's inset back. 16 below 700, with the
        rest of the table. */
.levels__cell       { flex: 1; min-width: 0; padding: 8px 0; text-align: center;
                      box-sizing: border-box; cursor: pointer;
                      font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 600; line-height: 1.25;
                      background: var(--surface); border: 1px solid var(--line-strong);
                      color: var(--text-label); }
.levels__cell--current { border: 1.5px solid var(--text); color: var(--text); }

/* THE FILL IS :checked, NOT A CLASS THE SERVER WRITES. A pending mark painted from
   the context could only appear after a round trip, so pressing a cell moved the
   clipped radio and showed nothing until the preview: a control whose state is
   rendered server-side is a control with no state between presses.

   :has() gives both marks with no script. --current stays on the level in force, so
   a row mid-edit still reads as what it is and what it will be, and at rest the two
   coincide and only the fill shows.

   .levels__cell--pending is RETIRED: nothing writes it and nothing should. The
   template no longer emits it. */
.levels__cell:has(.levels__input:checked) { background: var(--evidence);
                      border: 1px solid var(--evidence); color: var(--surface); }

/* Where :has() is unsupported the fill never appears, so the FOCUS ring is the
   fallback: a press focuses the radio, so the cell somebody just chose is the
   ringed one. Weaker than the fill and not nothing, which is the state this
   replaced. */
.levels__input:checked:focus-visible + span,
.levels__input:focus + span { outline: 2px solid var(--text); outline-offset: 3px; }
.levels__input      { position: absolute; width: 1px; height: 1px; padding: 0;
                      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
                      border: 0; }
.levels__input:focus-visible + span { outline: 2px solid var(--text);
                      outline-offset: 3px; }

@media (max-width: 700px) {
  /* The cells keep their row rather than stacking: four ordinal steps read as a
     sequence side by side and as a list not at all. They are 44px tall here,
     because at this width they are the page's primary control. */
  .table--levels    { grid-template-columns: 52px minmax(0, 1fr); }
  .levels           { grid-column: 1 / -1; padding: 11px 16px; }
  .levels__cell     { padding: 14px 0; }
}

/* ---- Crumb, and a mono run ------------------------------------------- *
 * Both were used on three tenant templates and declared only in
 * console.css, so on a tenant page they rendered as nothing: a crumb with
 * no layout and a code in the body face at body size.
 *
 * A class used by two products belongs to both files, not borrowed from
 * one. The values are identical on purpose, because a breadcrumb means the
 * same thing in both places and a second shape would be a second pattern.
 *
 * .mono is the tenant name for what the console calls .console__mono. Not
 * a rename there: the console keeps its own, and the sweep that found this
 * now asserts no tenant template names a console class.               */

.crumb              { display: flex; align-items: center; gap: 9px;
                      flex-wrap: wrap; font-size: var(--size-body-sm); }
.crumb__sep         { color: var(--unrecorded); }
.crumb__here        { color: var(--text-secondary); }

.mono               { font-family: var(--font-mono); font-size: var(--size-label);
                      font-weight: 600; color: var(--text-secondary); }
