/**
 * shell — the console's global chrome and its global states.
 *
 * Screen 01 is not a view. It is the frame every other view is read inside: the printed
 * header, the section tabs, and the four states a screen can be in before it has any
 * content of its own — loading, empty, failed, and refused. Those are styled once, here,
 * so that a reviewer meets the same vocabulary on the twelfth screen as on the first.
 *
 * Shared primitives live in style.css; this file restates only what the approved design
 * changes and adds only what the design shows and style.css has no name for. Everything
 * is a token — there is not a hex in this file.
 *
 * The header is a printed header: full width, square, one hairline at its foot, no blur
 * and no translucency. The tabs set in the label face, because a section mark is a label
 * and not prose.
 */

/* --------------------------------------------------------------------------
   Skip link

   Twelve tabs sit between the top of the document and the first control inside a
   view. A keyboard reader should not have to walk all twelve to reach the screen
   they are already looking at. Off-canvas until focused, then printed in place.
   -------------------------------------------------------------------------- */

#shell-skip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 40;
  transform: translateY(-120%);
  display: inline-block;
  padding: 10px var(--md);
  background: var(--paper-lit);
  color: var(--ink);
  border: 1px solid var(--ink);
  border-top: 0;
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
}
#shell-skip:focus-visible {
  transform: translateY(0);
  outline: 2px solid var(--ink);
  outline-offset: -4px;
}

main:focus { outline: none; }
main:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }

/* --------------------------------------------------------------------------
   The header
   -------------------------------------------------------------------------- */

#chrome {
  position: sticky;
  padding-bottom: 0;
}

/* The wordmark is the one piece of display type in the chrome. */
#chrome > strong {
  font-size: 1.125rem;
  font-weight: 650;
  font-variation-settings: 'wdth' 112;
  letter-spacing: -0.02em;
  line-height: 1.2;
  flex: 0 0 auto;
}

/* The mark takes `currentColor` rather than a fill of its own, so it inverts with the
   text colour of whatever surface it is placed on and never needs a reversed copy. */
#chrome > strong.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--xs);
}

#chrome .brand-mark {
  display: block;
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
}

/* --------------------------------------------------------------------------
   The tablist

   Twelve sections do not fit on a 320px phone and never will. They scroll, they do
   not truncate: every tab stays in the document, in the tab order, and reachable by
   arrow key. The scroll gets a printed indicator below, because a scroll region with
   no affordance is a hidden control.
   -------------------------------------------------------------------------- */

#chrome nav {
  align-self: stretch;
  align-items: stretch;
  scroll-behavior: smooth;
  /* The indicator below is the affordance; a second, transient one is noise. */
  scrollbar-width: none;
}
#chrome nav::-webkit-scrollbar { display: none; }

#chrome nav button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 12px;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  border-bottom: 2px solid transparent;
  transition: background-color 120ms linear, color 120ms linear, border-color 120ms linear;
}

/* Tonal layering, not tint: the header sits on paper-lit, hover presses to paper-sunk,
   and the selected tab lifts back to paper. No shadow is involved in any of it. */
#chrome nav button:hover { background: var(--paper-sunk); color: var(--ink); }
#chrome nav button.active,
#chrome nav button[aria-selected='true'] {
  background: var(--paper);
  color: var(--ink);
  border-bottom-color: var(--ink);
  font-weight: 700;
}

/* The nav is a scroll container, so an outset ring is clipped on both axes. Inset it. */
#chrome nav button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -4px;
}

/* The scroll indicator: a printed track with an ink thumb, set by shell.js against
   the tablist's real geometry. Removed from the accessibility tree — it reports a
   position, and the position is already carried by focus and by the selected tab. */
#shell-navscroll {
  position: absolute;
  bottom: 0;
  height: 2px;
  background: var(--paper-sunk);
  pointer-events: none;
}
#shell-navscroll > i {
  display: block;
  height: 100%;
  background: var(--ink);
  font-style: normal;
}

/* --------------------------------------------------------------------------
   The scoped header

   A reviewer — often an external consultant — is given one section. A header with a
   single tab and eleven silent absences looks broken, so the scope is stated. It is
   marked aria-hidden because #who already announces the same role, and a tablist
   should own tabs.
   -------------------------------------------------------------------------- */

#chrome[data-scope='single'] { height: 72px; }
#chrome[data-scope='single'] nav {
  flex-wrap: wrap;
  align-content: flex-end;
  padding-top: 12px;
}
#shell-scope {
  flex: 1 0 100%;
  align-self: flex-start;
  padding: 0 var(--md) 2px;
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink-secondary);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Identity and sign out

   The address is an identified value, so it sets in mono. Sign out is a ghost
   button in the design — a bordered box, not an underlined word, because it ends
   the session and should read as a control.
   -------------------------------------------------------------------------- */

#who {
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  min-width: 0;
}

#chrome #signout,
#chrome #signout.link {
  flex: 0 0 auto;
  padding: 8px 14px;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
}
#chrome #signout:hover,
#chrome #signout.link:hover { background: var(--paper-sunk); color: var(--ink); }

/* --------------------------------------------------------------------------
   Loading

   A rule that is part-drawn is the loading state: a paper-sunk track with an ink
   segment travelling it. Under prefers-reduced-motion style.css stops the travel and
   the segment stands still — a partly drawn rule still reads as partly done, and the
   word "Loading" is printed either way. Motion is never the only carrier.
   -------------------------------------------------------------------------- */

.loading {
  position: relative;
  display: block;
  border-top: 2px solid var(--paper-sunk);
  padding-top: var(--sm);
  font-size: 0.75rem;
  color: var(--ink-secondary);
}
.loading::before {
  content: '';
  position: absolute;
  top: -2px;
  left: 0;
  width: 34%;
  height: 2px;
  background: var(--ink);
  animation: shell-draw 1500ms ease-in-out infinite;
}
@keyframes shell-draw {
  0%   { left: 0; }
  50%  { left: 66%; }
  100% { left: 0; }
}

/* The line shell.js prints for a view whose loader says nothing of its own. */
.sh-busy {
  margin: 0 0 var(--md);
  max-width: none;
}

/* --------------------------------------------------------------------------
   Bones

   A bar with the shape of a value and none of its content, used to reserve a layout
   before its data lands. A sunk field with a ruled underside: the same two tokens a
   filled cell uses, drawn rather than filled in — which is why it does not need to be
   dimmed or shimmered to read as unfinished.

   Static, deliberately. The one moving thing while a screen loads is the progress rule
   under the header. Forty shimmering cells is a busy screen imitating a fast one, and
   under prefers-reduced-motion style.css would stop them all anyway, so the still
   state is the one that has to work.
   -------------------------------------------------------------------------- */

.sh-bone {
  display: block;
  height: 0.6875rem;
  margin: 0.25rem 0;
  background: var(--paper-sunk);
  border-bottom: 1px solid var(--rule);
}
.sh-bone-short { width: 62%; }

/* The rows shell.js puts into a table that has not been filled yet. Only into an empty
   one: a screen being refreshed keeps the data it is showing, because last known
   figures under a moving progress rule beat a grid of grey bars where the numbers were.
   `aria-hidden` on the row, since `aria-busy` and the word "Loading" already say this. */
.sh-skeleton-row td {
  padding-top: var(--sm);
  padding-bottom: var(--sm);
}

/* --------------------------------------------------------------------------
   A control that has been pressed and is working

   app.js sets `aria-busy` on it for the duration of the request. It keeps its label —
   a button whose text is swapped for "Saving…" loses the word that said what it does,
   and the label is what someone reads to check they pressed the right one — and takes
   the part-drawn rule underneath instead.

   `pointer-events: none` is the visible half of the lock only. The lock itself is a
   flag app.js checks before it calls the handler, so a second Enter on a focused
   control is refused the same way a second click is.
   -------------------------------------------------------------------------- */

button[aria-busy='true'] {
  position: relative;
  cursor: progress;
  pointer-events: none;
}
button[aria-busy='true']::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 34%;
  height: 2px;
  background: var(--ink);
  animation: shell-draw 1500ms ease-in-out infinite;
}

/* A busy screen says so to assistive technology through aria-busy, and to a cursor
   here. It is never dimmed — reading text is not dimmed to express state. */
.view[aria-busy='true'] { cursor: progress; }

/* The progress rule: the same figure `.loading` draws, at chrome scale — and on the
   same edge, since `.loading` puts its part-drawn rule along the top of what it
   describes. Indeterminate, because none of these routes reports progress and a bar
   that invents a percentage is lying about what it knows.

   The top edge and not the bottom one: `#shell-navscroll`, the tablist's own
   scroll affordance, is a 2px rule along the chrome's bottom edge, and a second 2px
   rule in the same band read as one indicator behaving strangely rather than as two
   indicators. They are different facts and they do not share a line.

   2px and `pointer-events: none`, so it is never in the way of the tablist below it.
   It is not an overlay for the reason stated above: text is not dimmed here, and
   blocking the nav would trap someone on the screen they are waiting for.

   Positioned against `#chrome`, which is already `position: sticky` above, so the rule
   rides with the header rather than needing a second fixed layer of its own. */
.sh-progress {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  background: var(--paper-sunk);
  overflow: hidden;
  pointer-events: none;
}
.sh-progress[hidden] { display: none; }

.sh-progress::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 34%;
  height: 2px;
  background: var(--ink);
  animation: shell-draw 1500ms ease-in-out infinite;
}

/* --------------------------------------------------------------------------
   Failure

   app.js prints "This screen did not load." and the server's sentence. shell.js sets
   that into the design's shape: a drawn warning mark, the sentence, and the retry.
   -------------------------------------------------------------------------- */

.view-error {
  padding: var(--lg) var(--md);
  max-width: none;
}
.view-error p { max-width: 68ch; }

.sh-error-head {
  display: flex;
  align-items: flex-start;
  gap: var(--sm);
  margin: 0 0 var(--sm);
  color: var(--mark-ink);
}
.sh-error-head strong {
  font-family: var(--display);
  font-size: 1.0625rem;
  font-weight: 700;
  font-variation-settings: 'wdth' 106;
  letter-spacing: -0.01em;
  line-height: 1.25;
}
/* Drawn, in currentColor. There are no icon fonts in this console. */
.sh-mark {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-top: 2px;
}

/* The server's sentence. Prose sets in Archivo; a status or a code is a measured
   value and sets in mono, on its own plate. */
.sh-error-detail {
  margin: 0 0 var(--md);
  font-size: 0.9375rem;
  color: var(--ink);
}
.sh-error-detail.sh-measured {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: -0.01em;
  background: var(--paper-lit);
  border: 1px solid var(--rule);
  padding: var(--sm) 10px;
}

.sh-error-note {
  margin: 0 0 var(--md);
  font-size: 0.875rem;
  color: var(--ink-secondary);
}
.sh-error-note code {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--ink);
}

.view-error button { padding: 12px 22px; }

/* --------------------------------------------------------------------------
   Refusal

   A permission failure is not a breakage, and saying "this screen did not load"
   about one is a lie of omission. It gets its own heading, the role the server
   actually asked for, and the identity that was refused.
   -------------------------------------------------------------------------- */

.view-error.sh-denied {
  background: var(--paper-lit);
  border-left: 2px solid var(--ink);
}
.sh-denied .sh-error-head { color: var(--ink); }

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

   Empty states are sentences. The vocabulary is stated once here so every view
   spells absence the same way, and so that "nothing here" is never a blank rule.
   -------------------------------------------------------------------------- */

.empty {
  background: var(--paper-lit);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--rule);
  padding: var(--lg) var(--md);
  margin-bottom: var(--lg);
  max-width: none;
}
.empty .empty-mark {
  display: block;
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  margin-bottom: var(--sm);
}
.empty p {
  margin: 0 0 var(--sm);
  font-size: 0.9375rem;
  max-width: 62ch;
}
.empty p:last-child { margin-bottom: 0; }
.empty .muted { color: var(--ink-secondary); }

/* app.js writes an empty queue as a muted list row. `.list li` is a grid, which broke
   the sentence into one word per line; an empty state is prose and sets as prose. */
.list > li.muted {
  display: block;
  padding: var(--lg) var(--md);
  font-size: 0.9375rem;
  color: var(--ink-secondary);
  border-bottom: 1px solid var(--rule);
}
.list > li.loading { padding: var(--md); }

/* --------------------------------------------------------------------------
   The standing ruleset warning

   This is not an error toast. It is the system stating the limit of what it knows:
   the grade-ceiling schedule was read off an OCR-degraded PDF and has not been
   hand-keyed, so the matcher is declining to emit a hard "not eligible" on a ceiling
   comparison. It is set as a notice — a title, then the statement — rather than as a
   one-line strip, because it is the console's single most important operating caveat.
   -------------------------------------------------------------------------- */

#ruleset-banner {
  align-items: flex-start;
  gap: var(--md) var(--lg);
  padding: var(--md) var(--lg);
  border-top: 1px solid var(--ink);
  border-left: 2px solid var(--ink);
}
#ruleset-banner > strong {
  flex: 0 0 auto;
  font-family: var(--display);
  font-size: 1.0625rem;
  font-weight: 700;
  font-variation-settings: 'wdth' 106;
  letter-spacing: -0.01em;
  line-height: 1.3;
}
#ruleset-banner #ruleset-banner-text {
  flex: 1 1 340px;
  font-size: 0.875rem;
  line-height: 1.5;
  max-width: 78ch;
}
/* The verdict term is a cited value, not emphasis. Only present before app.js
   rewrites the sentence — see the gaps file. */
#ruleset-banner em {
  font-family: var(--mono);
  font-size: 0.8125rem;
  font-style: normal;
  text-decoration: none;
}
#ruleset-banner > #ruleset-verify { margin-left: auto; }
#ruleset-banner > #ruleset-verify[hidden] + #ruleset-dismiss { margin-left: auto; }
#ruleset-banner button {
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 8px 14px;
}
/* Ink outlines vanish on the marking-ink ground. The ring takes the paper instead. */
#ruleset-banner button:focus-visible {
  outline: 2px solid var(--paper-lit);
  outline-offset: 3px;
}

/* The banner is fixed to the foot of the window, so it would otherwise sit on top of
   the last rows of a long table. shell.js measures it; nothing happens if it cannot. */
body { padding-bottom: var(--shell-banner-h, 0px); }

/* --------------------------------------------------------------------------
   Narrow

   320px is a normal case: reviewers work from phones in the field. The header stops
   being one row and becomes two — identity above, sections below — and the tablist
   runs the full width with its indicator under it. Nothing is dropped and nothing
   is truncated; the address wraps rather than ending in an ellipsis, because half an
   address is worse than two lines of one.
   -------------------------------------------------------------------------- */

@media (max-width: 860px) {
  #chrome,
  #chrome[data-scope='single'] {
    height: auto;
    min-height: 56px;
    flex-wrap: wrap;
    align-items: center;
    row-gap: var(--sm);
    padding: var(--sm) var(--md) 0;
  }

  #chrome > strong { font-size: 1rem; order: 1; margin-right: auto; }
  #who { order: 2; font-size: 0.6875rem; white-space: normal; overflow-wrap: anywhere; text-align: right; }
  #chrome #signout, #chrome #signout.link { order: 3; }

  #chrome nav,
  #chrome[data-scope='single'] nav {
    order: 4;
    flex: 1 0 100%;
    height: 44px;
    align-self: auto;
    padding-top: 0;
    margin: 0 calc(var(--md) * -1);
    padding-left: var(--md);
    padding-right: var(--md);
  }
  #chrome nav button { padding: 0 12px; }

  /* The scope marker cannot sit above a full-width tab row without pushing the
     header again, so on a phone it sits inline, ahead of the one tab it explains. */
  #chrome[data-scope='single'] nav { flex-wrap: nowrap; }
  #shell-scope {
    flex: 0 0 auto;
    align-self: center;
    padding: 0 var(--sm) 0 0;
  }

  #ruleset-banner { padding: var(--md); gap: var(--sm); }
  #ruleset-banner > #ruleset-verify,
  #ruleset-banner > #ruleset-verify[hidden] + #ruleset-dismiss { margin-left: 0; }
  .view-error { padding: var(--md); }
}

@media (max-width: 380px) {
  #chrome > strong { font-size: 0.9375rem; }
  #chrome #signout, #chrome #signout.link { padding: 8px 10px; }
  #chrome nav button { padding: 0 10px; letter-spacing: 0.06em; }
}
