/* AI Talent Assessment portal.

   The reading surfaces are set as documents: a single measured column, letterspaced small-caps
   labels, nothing decorative. The CONVERSATION is the exception and it is deliberate. It was
   originally styled as a record being made, on the argument that the output is evidence and the
   layout should say so. That argument lost to a simpler one: people type more, and more concretely,
   into something they recognise as a chat. So the transcript is agent-left, participant-right
   bubbles, and the honesty the old layout was carrying is carried by words instead: the opening turn
   and the consent page both state that assessors read this afterwards.

   Palette is the Chin Hin logo, sampled from the artwork rather than transcribed: navy #27245C and
   red #D62027 are 95% and 5% of the pixels in `Chin Hin logo.png`, and the ration is the point.
   Navy carries structure and every ordinary emphasis. Neutrals are biased toward navy-violet rather
   than being pure grey, so they read as chosen.

   THREE SEMANTIC COLOURS, ONE MEANING EACH. Before this they were tangled: amber simultaneously
   meant reassurance to a participant, a failed reliability threshold, "OPEN" on a hundred rows, and
   an erasure. A colour that means four things carries no signal.

     --accent   navy   structure, actions, selection            everywhere
     --warn     amber  incomplete, a gap, never "bad"           assessor and admin only
     --danger   red    irreversible or failed                   assessor and admin only

   RED NEVER REACHES A PARTICIPANT. Its participant-facing job would have been the reassurance
   callouts that say "no score comes out of this" and "nothing here affects your role, pay or
   progression". Red is the error convention on the web and it is read before the words are, so a
   red rule around the most reassuring sentence in the product inverts it. Those callouts are navy.

   DARK IS DERIVED, NOT BRAND-EXACT, AND IT HAS TO BE. Logo navy on a dark ground is 1.36:1 and logo
   red is 3.64:1; both are illegible. Navy lifts to periwinkle and red lifts to a warm rose, which is
   also why the white-font logo artwork exists and is used there. Every pair below was measured, and
   every text pair clears WCAG AA 4.5:1 in both themes. */

:root {
  color-scheme: light dark;

  --ink:        #15152b;   /* 17.89:1 on surface */
  --ink-soft:   #45456b;   /*  9.07:1 */
  --ink-faint:  #5c5c82;   /*  6.35:1 on surface, 5.89:1 on paper. Was 3.52/3.22 and failed AA. */
  --paper:      #f6f6fa;
  --surface:    #ffffff;
  --rule:       #dbdbe8;   /* decorative separators only */
  --rule-soft:  #eaeaf2;
  --edge:       #7c7c95;   /* borders on INTERACTIVE controls: 4.06:1, clears the 3:1 requirement */

  --accent:     #27245c;   /* Chin Hin navy, sampled from the logo */
  --accent-ink: #ffffff;
  --accent-wash:#ebeaf3;

  --warn:       #8a5a12;
  --warn-wash:  #fbf1e0;

  --danger:     #d62027;   /* Chin Hin red, sampled from the logo */
  --danger-wash:#fdf3f4;

  --measure: 62ch;
  --radius: 3px;

  --masthead-h: 4.25rem;   /* a MINIMUM for the masthead, not a height anything subtracts from */

  --serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
  --sans:  ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --mono:  ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
}

/* Dark is defined once, here, and referenced by both the OS query and the explicit toggle. Three
   copies of the same hex values is how a theme drifts out of sync with itself. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink:        #ececf4;
    --ink-soft:   #b2b2c8;
    --ink-faint:  #9797b0;
    --paper:      #111120;
    --surface:    #1a1a2c;
    --rule:       #33334c;
    --rule-soft:  #26263a;
    --edge:       #6b6b8a;

    --accent:     #a8a3e2;   /* logo navy lifted: brand navy is 1.36:1 on this ground */
    --accent-ink: #111120;
    --accent-wash:#232040;

    --warn:       #dda85e;
    --warn-wash:  #2b2214;

    --danger:     #f2777c;   /* logo red lifted: brand red is 3.64:1 on this ground */
    --danger-wash:#2c1a1e;
  }
}

/* The viewer's own choice wins over the OS preference, in both directions. */
:root[data-theme="dark"] {
  --ink: #ececf4; --ink-soft: #b2b2c8; --ink-faint: #9797b0;
  --paper: #111120; --surface: #1a1a2c; --rule: #33334c; --rule-soft: #26263a; --edge: #6b6b8a;
  --accent: #a8a3e2; --accent-ink: #111120; --accent-wash: #232040;
  --warn: #dda85e; --warn-wash: #2b2214;
  --danger: #f2777c; --danger-wash: #2c1a1e;
}

* { box-sizing: border-box; }

/* `hidden` must win. Any element given `display` by a class (a grid row, a flex item) silently
   ignores the attribute otherwise, which is how a "thinking" indicator ends up on screen for the
   whole conversation. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}

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

.masthead {
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
.masthead .inner {
  max-width: var(--frame); margin: 0 auto; padding: .85rem 1.5rem;
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  min-height: var(--masthead-h);
}

.wordmark { display: flex; align-items: center; gap: .7rem; text-decoration: none; color: var(--ink); }
.wordmark img { height: 1.75rem; width: auto; display: block; }
.wordmark .what {
  font: 600 .6875rem/1 var(--sans); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-faint); padding-left: .7rem; border-left: 1px solid var(--rule);
}
/* On the chat screen the wordmark is not a link: it was the one tappable thing at the top of the
   screen mid-answer, and it threw people to the sign-in page with no warning. */
.wordmark[aria-disabled="true"] { pointer-events: none; }

/* Two logo files, not one recoloured: the white-font artwork is the supplied asset for dark grounds,
   and logo red is 3.64:1 there, which is why a simple filter would not do. */
.logo-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .logo-light { display: none; }
  :root:not([data-theme="light"]) .logo-dark  { display: block; }
}
:root[data-theme="dark"] .logo-light { display: none; }
:root[data-theme="dark"] .logo-dark  { display: block; }
:root[data-theme="light"] .logo-light { display: block; }
:root[data-theme="light"] .logo-dark  { display: none; }

.masthead nav { margin-left: auto; display: flex; gap: 1.25rem; align-items: center; }
/* "Finish later" sits here rather than under the composer, so it does not scroll away
   mid-answer. A form, not a link, so it works with JavaScript off. */
.masthead-form { margin: 0; display: flex; }
.masthead-form button { min-height: 2.25rem; padding: 0 .75rem; font-size: .875rem; }
.masthead nav a { color: var(--ink-soft); text-decoration: none; font-size: .875rem; }
.masthead nav a:hover { color: var(--accent); }

/* At 375px the logo plus the "AI+ ASSESSMENT" label plus the toggle wrapped to two rows, costing
   40px of a phone screen on the one page where vertical space is the scarce resource. The label is
   the least load-bearing of the three: the page's own h1 already says what this is. */
@media (max-width: 30rem) {
  .masthead .inner { flex-wrap: nowrap; gap: .6rem; padding: .7rem 1.1rem; min-height: 3.25rem; }
  .wordmark .what { display: none; }
  .wordmark img { height: 1.5rem; }
  .masthead nav { gap: .6rem; }
}

/* 44px minimum on anything you tap with a thumb. Both masthead controls were 36px, which is the
   default the rest of the app already overrides everywhere else. Applied by input type rather than
   width: a small window on a laptop has a mouse and does not need the extra height. */
@media (pointer: coarse) {
  .masthead-form button, .theme-toggle { min-height: 2.75rem; min-width: 2.75rem; }
}

/* Theme toggle. The dark/light custom properties existed and nothing ever set `data-theme`, so the
   blocks were dead code and the app followed the OS with no way to override it. */
.theme-toggle {
  appearance: none; background: none; border: 1px solid var(--edge);
  border-radius: var(--radius); color: var(--ink-soft);
  min-height: 2.25rem; min-width: 2.25rem; padding: 0 .5rem;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; font: inherit; line-height: 1;
}
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme-toggle svg { width: 1.05rem; height: 1.05rem; fill: none;
                    stroke: currentColor; stroke-width: 1.6; }
.theme-toggle .i-sun { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .i-sun  { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .i-moon { display: none; }
}
:root[data-theme="dark"] .theme-toggle .i-sun  { display: block; }
:root[data-theme="dark"] .theme-toggle .i-moon { display: none; }
:root[data-theme="light"] .theme-toggle .i-sun  { display: none; }
:root[data-theme="light"] .theme-toggle .i-moon { display: block; }

/* ONE variable decides the page width, and the masthead, the content and the footer all read it.
   They used to carry their own numbers, and they drifted: the masthead was pinned at 76rem while a
   `main.reading.split` page ran at 88rem and the landing page ran full width, so on a 1900px window
   the logo sat 342px right of the content and the toggle 390px in from the edge. A `:has()` rule
   meant to fix an earlier version of this made it worse, because `main.reading.split` also matches
   `main.reading` and was being capped to 52rem.

   Set --frame per page type. Nothing else may hardcode a page width. */
:root { --frame: 76rem; }
body:has(main.reading)        { --frame: 52rem; }
body:has(main.reading.narrow) { --frame: 40rem; }
body:has(main.reading.split)  { --frame: 88rem; }
body:has(main.bench)          { --frame: 88rem; }
body.is-chat                  { --frame: 62rem; --chat-measure: 76ch; }
body.is-landing               { --frame: none; }    /* the window IS the frame */

main { max-width: var(--frame); margin: 0 auto; padding: 2.5rem 1.5rem 5rem; }

/* 1.5rem is a phone gutter. Once the layout stops being one narrow column and starts filling the
   window, the same 24px leaves text almost touching the screen edge. */
@media (min-width: 62rem) {
  main, footer { padding-left: 3rem; padding-right: 3rem; }
  .masthead .inner { padding-left: 3rem; padding-right: 3rem; }
  /* Chat aligns to its transcript column, which keeps a narrower gutter. */
  body.is-chat .masthead .inner { padding-left: 1.25rem; padding-right: 1.25rem; }
}

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

   A DESKTOP IS NOT A WIDER PHONE, AND THE FIX IS NOT A WIDER COLUMN. A single 40rem column leaves
   half of a 1440px screen empty, which is what this addresses. But stretching the prose to fill that
   space pushes lines past 100 characters, and long lines are harder to read, not easier: the eye
   loses its place on the return sweep. So the extra width buys a SECOND COLUMN, and every column
   keeps a readable measure.

   `.page` is the pattern: a primary reading flow, and an aside carrying the thing the reader acts on.
   One column on a phone in DOM order, two side by side from 62rem. `.pair` splits a run of
   equal-weight material (the two "how" lists) once there is room for both. */


.page { display: grid; gap: 2rem; }
.page > * { min-width: 0; }
@media (min-width: 62rem) {
  .page {
    /* The aside is allowed to GROW rather than being pinned narrow. Pinning it meant the flow
       column inherited all the slack on a mid-size screen and ran to 85 characters. Letting the
       aside take some keeps the prose near its measure at every width in between. */
    grid-template-columns: minmax(0, 1fr) minmax(19rem, 27rem);
    gap: 3rem 4rem;
    align-items: start;
  }
  /* The header spans, so the title is not squeezed into one column. */
  .page > .page-head { grid-column: 1 / -1; }
}

/* `.pair` groups the two "how" lists. IT DELIBERATELY DOES NOT SPLIT THEM INTO COLUMNS, and the
   reason is worth keeping because the instinct is the other way.

   Two columns inside the flow was the first attempt. It measured 31 characters per column at 62rem,
   so the layout had swapped empty margin for cramped text. Pushing the split out to a wider
   breakpoint did not rescue it either: a media query tests the VIEWPORT, while the width actually
   available is capped by the page container, so at a 1920px viewport the columns still came out at
   37 characters. With an aside taking its share there is no window width at which the flow column
   can hold two comfortable measures inside a page that is sensibly capped.

   So the lists stack, and the flow column carries one column at 62 characters. The desktop width is
   spent on the aside instead, which is where the reader acts. */
.pair { display: grid; gap: 0; }
.pair > * { min-width: 0; }

/* ------------------------------------------------------------------ landing

   The sign-in screen is the one page that should fill a desktop window rather than sit in a column,
   because it has little content and is the first thing anyone sees. Full-height split: what this is
   and the form on the left, Menara Chin Hin on the right.

   The photograph is documented in `static/IMAGE-CREDITS.md`, including the licence it is used under
   and why four other candidates were rejected. `object-position` favours the left of frame, where the
   room is, so a tall crop does not centre on the empty floor. */

body.is-landing { min-height: 100dvh; display: flex; flex-direction: column; }
body.is-landing .masthead { flex: none; }
/* login.html empties the footer block, but the element still carried its 3rem bottom padding, which
   left a dark strip under the photo on a full-height layout. */
/* Templates that empty the footer block still rendered the element, and its 3rem bottom padding
   left a strip of page under the content. This was fixed for the landing page and not
   generalised, so the chat screen kept a 48px band below the composer. `:empty` will not match,
   because an emptied Jinja block leaves whitespace behind, so the collapse is by body class. */
body.is-landing footer,
body.is-chat footer { display: none; }

main.landing { max-width: none; margin: 0; padding: 0; flex: 1; display: flex; min-height: 0; }
.landing-grid { display: flex; flex-direction: column; width: 100%; }

.landing-art { flex: none; position: relative; overflow: hidden; background: var(--accent); }
.landing-art img { width: 100%; height: 100%; object-fit: cover; object-position: 42% center;
                   display: block; }
/* A navy veil, so the photo reads as a brand surface rather than a stock picture, and so white type
   over it would hold up if any is ever added. */
.landing-art::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(160deg, rgba(39,36,92,.34), rgba(39,36,92,.08) 55%, rgba(39,36,92,.30));
}

.landing-copy {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; justify-content: center;
  gap: 1.25rem; padding: 2.5rem 1.5rem 3rem;
}
.landing-copy > * { max-width: 34rem; }
.landing-copy h1 { margin: 0; }

/* Phone: a band across the top, sized so it is a picture and not a wallpaper.
   `order` rather than DOM order: the markup keeps the form first so a screen reader and a no-CSS
   render both reach the thing the page is for before a decorative photograph. */
@media (max-width: 61.99rem) {
  .landing-art { order: -1; height: 38vw; min-height: 150px; max-height: 240px; }
  .landing-copy { padding-top: 2rem; }
}

@media (min-width: 62rem) {
  .landing-grid { flex-direction: row; align-items: stretch; }
  .landing-copy { flex: 1 1 46%; padding: 3rem 3rem 3rem; }
  .landing-art { flex: 1 1 54%; height: auto; }
  /* The window is now the frame, so the page itself must not scroll. */
  body.is-landing { height: 100dvh; overflow: hidden; }
}

/* The aside is where the action lives. Not sticky: the consent checkbox says "I have read the
   above", and pinning it beside unread text would make that sentence untrue. */
.aside { display: flex; flex-direction: column; gap: 1.25rem; }
.aside .callout { margin: 0; }
.aside .panel { margin: 0; }
@media (min-width: 62rem) {
  .aside { border-left: 1px solid var(--rule); padding-left: 2rem; }
}

footer {
  max-width: var(--frame); margin: 0 auto; padding: 0 1.5rem 3rem;
  color: var(--ink-faint); font-size: .8125rem;
}

/* ------------------------------------------------------------------ type */

h1 {
  font-family: var(--serif); font-weight: 400; font-size: 1.9rem; line-height: 1.25;
  margin: 0 0 .4rem; text-wrap: balance;
}
h2 {
  font-family: var(--serif); font-weight: 400; font-size: 1.3rem;
  margin: 2.5rem 0 .75rem; text-wrap: balance;
}
h3 { font-size: .95rem; margin: 1.75rem 0 .5rem; }
/* The measure belongs to PROSE, not to every `p` on the page. As a bare element selector it capped
   paragraphs at 62ch inside 1230px admin and console panels whose tables ran full width, so half of
   every panel sat empty and no two measures lined up. It also stranded the chat composer's note
   188px short of the composer edge on desktop. */
p { margin: 0 0 1rem; }
main.reading p, .callout p, .disclosure p, .field p, form p { max-width: var(--measure); }
.lede { color: var(--ink-soft); font-size: 1.05rem; max-width: var(--measure); }

.eyebrow {
  font: 600 .6875rem/1.4 var(--mono);
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint);
  margin: 0 0 .75rem;
}
.note { color: var(--ink-soft); font-size: .875rem; max-width: var(--measure); }

a { color: var(--accent); }

/* ------------------------------------------------------------------ transcript

   Patterned on how Claude, ChatGPT and Gemini actually build a conversation, because all three
   converged on the same shape and the convergence is the useful part.

   THE ASYMMETRY IS THE MAIN BORROWING, and it refines rather than reverses the agent-left,
   participant-right instruction. None of the three puts symmetrical bubbles on both sides. The
   assistant's text sits on the page ground like a document; only the HUMAN turn gets a container.
   Three reasons, and all of them apply harder here than to a general assistant:

     1. Agent turns are the long ones. This one opens with a nine-line briefing, and a border drawn
        around a long passage fights the reading instead of helping it.
     2. Asymmetry encodes who is speaking without spending a colour or a label on it.
     3. It lets the agent's text run the full measure, where a bubble would inset it twice.

   WHAT ELSE CAME FROM THEM: the send control sits INSIDE the composer rather than beside it, the
   composer is one rounded container rather than a field plus a button, turns are separated by space
   rather than by rules, and the repeated speaker name is dropped once the layout already says who is
   talking. Labels survive for screen readers, which can read neither side nor colour.

   WHERE THEY DISAGREE WITH THE BRIEF, stated plainly: all three keep a narrow centred column and
   accept the empty margins, because line length matters more to reading than filling a window. This
   sits at 62rem as a compromise: wider than they are, narrower than the window. */

.transcript { margin: 1.25rem 0 0; display: flex; flex-direction: column; gap: 1.75rem; }

.utterance { display: flex; flex-direction: column; }
.utterance .said { white-space: pre-wrap; }

/* The agent: no container, full measure, page ground. */
.utterance.from-agent { align-items: flex-start; max-width: 100%; }
.utterance.from-agent .who {
  font: 600 .6875rem/1.6 var(--mono);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint);
  margin-bottom: .35rem;
}
/* A CONVERSATION CAN CARRY A LONGER LINE THAN A DOCUMENT, and this is where the chat page was
   wasting its width. `--measure` is 62ch, tuned for the consent and summary pages where somebody
   reads several hundred words in one go. Applied here it capped the agent at 625px inside a 992px
   frame, so its text stopped 327px short of the composer's right edge and the screen showed three
   different right edges.

   76ch is the top of the defensible range rather than the middle, and conversational turns earn it:
   they are two or three lines, so the return sweep that punishes long lines happens a handful of
   times per turn instead of continuously down a page. */
.utterance.from-agent .said { color: var(--ink); max-width: var(--chat-measure); }

/* The participant: a container, right-aligned, capped so a long answer still reads as one turn. */
.utterance.from-participant { align-items: flex-end; align-self: flex-end; max-width: 82%; }
/* Their name is redundant once the turn sits right in a filled container, so it is kept for assistive
   technology and taken off the screen. `.visually-hidden` is applied in the template. */
.utterance.from-participant .said {
  background: var(--accent-wash); color: var(--ink);
  padding: .8rem 1.05rem; border-radius: 14px; border-bottom-right-radius: 4px;
}

.utterance.thinking { align-items: flex-start; }

@media (max-width: 40rem) {
  .utterance.from-participant { max-width: 90%; }
}

/* ------------------------------------------------------------------ the conversation

   One continuous chat, not a stepped form. The transcript scrolls, the composer stays put, and
   there is deliberately no progress bar or topic counter: a count invites people to race it, and
   naming what is left tells them what is being looked at. The sense of moving through the
   conversation comes from the conversation. */

/* The chat screen is a flex column, NOT a viewport height minus a guessed masthead.
   `calc(100dvh - var(--masthead-h))` was the old approach and it was wrong in a way that only showed
   up on a phone: the masthead wraps to two rows at 375px, so it measures 108px against a 68px
   variable and the composer was pushed 40px below the fold, exactly where the send button lives.
   Letting the masthead take its own height and the chat take what remains has no number to get
   wrong. */
body.is-chat { height: 100dvh; overflow: hidden; display: flex; flex-direction: column; }
body.is-chat .masthead { flex: none; }

main.chat {
  max-width: none; margin: 0; padding: 0;
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
}
.chat-scroll {
  flex: 1; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column;
  /* The transcript scrolls and the composer does not, so the scrollbar narrowed one content box and
     not the other, and the two centred columns ended up 7px apart: half a scrollbar. Reserving the
     gutter on BOTH edges keeps the centre where it would be without a scrollbar, so the bubbles and
     the composer line up whether the conversation is long enough to scroll or not. Degrades to the
     old 7px offset where unsupported, which is the current behaviour rather than a new fault. */
  scrollbar-gutter: stable both-edges;
}
/* Reads --frame like every other frame element, so the masthead and the transcript column
   cannot end up the same width by coincidence and then drift apart. */
.chat-inner { max-width: var(--frame); margin: 0 auto; padding: 0 1.25rem; width: 100%; }
/* A short conversation sits just above the composer rather than stranded at the top of an empty
   screen. Once it outgrows the viewport this does nothing and normal scrolling takes over. */
.chat-scroll .chat-inner {
  padding-top: 1.75rem; padding-bottom: 1.5rem;
  margin-top: auto;
}
/* Above phone height, sit the conversation near the top and let it grow down. `margin-top: auto`
   only earns its place where the screen is short enough that a two-line opening would otherwise
   float alone at the top. */
@media (min-height: 44rem) {
  .chat-scroll .chat-inner { margin-top: 0; }
}

.composer {
  border-top: 1px solid var(--rule); background: var(--surface);
  padding: .85rem 0 max(.85rem, env(safe-area-inset-bottom));
  flex: none;
}
 /* One rounded container holding the field and the send control, rather than a bare field beside a
    separate button. The whole thing takes the focus ring, so focus reads as "the composer is active"
    instead of "this input is active". */
/* PILL, in the Gemini shape. The radius is measured, not picked: the composer is 57px tall at rest,
   so 28px is exactly half and gives a true stadium.

   NOT `border-radius: 999px`, which is the obvious way to write a pill and is wrong here. This
   composer grows to 177px as somebody writes a long answer, and at that height a 999px radius clamps
   to about 88px per corner and the field turns into a blob. A fixed 28px is a pill while it is one
   line and a well-rounded rectangle once it is not, which is the same thing Gemini's does. */
.composer-row {
  display: flex; align-items: flex-end; gap: .5rem;
  background: var(--surface); border: 1px solid var(--edge);
  border-radius: 28px; padding: .4rem .4rem .4rem 1.15rem;
}
.composer-row:focus-within { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.composer textarea {
  flex: 1; min-height: 2.25rem; max-height: 11rem; resize: none; overflow-y: auto;
  padding: .55rem 0; line-height: 1.5; max-width: none;
  background: none; border: 0; border-radius: 0;
}
.composer textarea:focus-visible { outline: none; }   /* the container shows focus instead */
.composer button {
  flex: none; min-height: 2.5rem; min-width: 2.5rem; padding: 0 1.15rem;
  border-radius: 20px;                       /* 40px tall, so this is its own true pill */
}

/* Thinking indicator. It exists because with a real model a turn takes seconds, and a page that
   simply sits there reads as broken rather than as busy. */
.utterance.thinking .said { display: flex; align-items: center; min-height: 1.6rem; }
.dots { display: inline-flex; gap: .3rem; }
.dots i {
  width: .4rem; height: .4rem; border-radius: 50%; background: var(--ink-faint);
  animation: pulse 1.3s ease-in-out infinite;
}
.dots i:nth-child(2) { animation-delay: .18s; }
.dots i:nth-child(3) { animation-delay: .36s; }
@keyframes pulse { 0%, 60%, 100% { opacity: .25; } 30% { opacity: 1; } }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}


/* The PDPA detail, folded away. Consent has to be available, not unavoidable: a page long enough
   to be skipped provides worse consent than a short one that is actually read. */
.disclosure {
  border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  padding: .35rem 0; margin: 1.75rem 0;
}
.disclosure > summary {
  cursor: pointer; padding: .8rem 0; font-size: .9375rem; color: var(--ink-soft);
  list-style-position: outside;
}
.disclosure > summary:hover { color: var(--accent); }
.disclosure > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.disclosure[open] > summary { color: var(--ink); margin-bottom: .5rem; }

.identity { text-align: center; padding: 1.75rem 1.5rem; }
.identity-name {
  font-family: var(--serif); font-size: 1.6rem; margin: 0 0 .2rem; max-width: none;
}
.identity .note { margin: 0; }

/* ------------------------------------------------------------------ compose */

.compose { margin-top: 2rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; }
textarea, input[type="text"], select {
  width: 100%; max-width: var(--measure);
  font: inherit; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule); border-radius: var(--radius);
  padding: .7rem .85rem;
}
textarea { min-height: 7.5rem; resize: vertical; line-height: 1.55; }
textarea:focus-visible, input:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
label { display: block; font-size: .875rem; color: var(--ink-soft); margin: 0 0 .3rem; }
.field { margin-bottom: 1.1rem; }

button, .button {
  font: 500 .9375rem/1 var(--sans);
  background: var(--accent); color: var(--accent-ink);
  border: 1px solid var(--accent); border-radius: var(--radius);
  padding: .7rem 1.15rem; cursor: pointer; text-decoration: none; display: inline-block;
}
button:hover, .button:hover { filter: brightness(1.08); }
/* Was bordered in --rule at 1.19:1, so "Back", "Not now", "Sign out" and "Erase" all read as
   disabled controls. --edge clears the 3:1 that interactive borders actually require. */
button.quiet, .button.quiet {
  background: transparent; color: var(--ink-soft); border-color: var(--edge);
}
button.quiet:hover, .button.quiet:hover { color: var(--accent); border-color: var(--accent); }

/* The one destructive control in the system. It permanently removes a transcript and a name, and it
   was styled identically to the Sign out button nine lines below it in the same template. */
button.danger, .button.danger {
  background: var(--danger); border-color: var(--danger); color: #ffffff;
}
button.danger:hover, .button.danger:hover { filter: brightness(1.06); }
button:disabled, button.danger:disabled {
  opacity: .45; cursor: not-allowed; filter: none;
}

button:focus-visible, .button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.actions { display: flex; gap: .75rem; align-items: center; margin-top: 1rem; flex-wrap: wrap; }

/* ------------------------------------------------------------------ progress */

.progress { display: flex; align-items: center; gap: .75rem; margin: 1.5rem 0 0; }
/* `flex: 1` is `flex: 1 1 0%`, and inside a shrink-to-fit parent that resolved to 0 x 3px: the
   assessor's progress bar had never been visible to anyone. An explicit basis fixes it. */
.progress .bar {
  flex: 1 1 12rem; min-width: 8rem; max-width: 18rem; height: 3px;
  background: var(--rule-soft); border: 1px solid var(--rule); border-radius: 2px;
  overflow: hidden;
}
.progress .bar i { display: block; height: 100%; background: var(--accent); }
.progress .count { font: 500 .75rem/1 var(--mono); color: var(--ink-faint); }
.progress + .note { margin-top: .9rem; }

/* ------------------------------------------------------------------ panels & tables */

.panel {
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 1.5rem; margin: 1.5rem 0;
}
.panel h2 { margin-top: 0; }

.scroll { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9375rem; }
th, td { text-align: left; padding: .6rem .75rem; border-bottom: 1px solid var(--rule-soft); }
th {
  font: 600 .6875rem/1.4 var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-faint); border-bottom: 1px solid var(--rule);
}
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); }

.pill {
  display: inline-block; font: 600 .6875rem/1 var(--mono);
  letter-spacing: .06em; text-transform: uppercase;
  padding: .35rem .5rem; border-radius: var(--radius);
  background: var(--accent-wash); color: var(--accent);
}
.pill.gap,
.pill.warn { background: var(--warn-wash); color: var(--warn); }
.pill.danger { background: var(--danger-wash); color: var(--danger); }
.pill.plain { background: transparent; color: var(--ink-faint); border: 1px solid var(--edge); }

.callout {
  border-left: 2px solid var(--accent); padding: .25rem 0 .25rem 1.25rem;
  margin: 1.5rem 0; color: var(--ink-soft); max-width: var(--measure);
}
/* `.amber` kept as an alias so no template silently loses its emphasis mid-rename. */
.callout.warn, .callout.amber { border-left-color: var(--warn); }
.callout.danger { border-left-color: var(--danger); }
.callout strong { color: var(--ink); font-weight: 600; }

ul.plain { list-style: none; padding: 0; margin: 0 0 1rem; max-width: var(--measure); }
ul.plain li { padding: .4rem 0; border-bottom: 1px solid var(--rule-soft); }
ul.terms { max-width: var(--measure); padding-left: 1.1rem; }
ul.terms li { margin-bottom: .55rem; }

/* ------------------------------------------------------------------ scoring bench

   Two columns: the evidence on the left, the standard and the score on the right. They sit side by
   side rather than on separate steps because a rater who has to remember the anchor while reading
   the transcript is scoring from memory of the anchor, which is exactly the drift the fixed rubric
   is supposed to prevent. */

main.bench { /* width comes from --frame, see body:has(main.bench) */ }

.bench-head {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 2rem;
  flex-wrap: wrap; padding-bottom: 1.25rem; border-bottom: 1px solid var(--rule);
}
.bench-head h1 { margin: 0; }
.bench-head .progress { margin: 0; }

.bench-grid {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 3rem; margin-top: 2rem; align-items: start;
}
@media (max-width: 62rem) { .bench-grid { grid-template-columns: 1fr; gap: 2.5rem; } }

.bench-col h2:first-of-type { margin-top: 0; }
.sticky { position: sticky; top: 1.5rem; }
@media (max-width: 62rem) { .sticky { position: static; } }

.transcript.compact .utterance { grid-template-columns: 5rem 1fr; padding: .8rem 0; }
.transcript.compact .said { font-size: .9375rem; }
.transcript.compact .from-agent .said { color: var(--ink-faint); font-size: .875rem; }

.utterance.citable { cursor: pointer; padding-left: .5rem; margin-left: -.5rem; border-radius: var(--radius); }
.utterance.citable:hover { background: var(--rule-soft); }
.utterance.citable .who {
  display: flex; align-items: center; justify-content: flex-end; gap: .4rem;
}
.utterance.citable .who .seq { font-variant-numeric: tabular-nums; }
.utterance.citable .who .lang {
  border: 1px solid var(--rule); border-radius: 2px; padding: 0 .25rem; font-size: .625rem;
}
.utterance.citable input[type=checkbox] { margin: 0; accent-color: var(--accent); }
.utterance.picked { background: var(--accent-wash); }
.utterance.picked .said { color: var(--ink); }

.anchor { border-left: 2px solid var(--rule); padding: .1rem 0 .1rem 1rem; margin: 0 0 1rem; }
.anchor p:last-child { margin: 0; font-size: .9375rem; color: var(--ink-soft); max-width: none; }
.anchor .eyebrow { margin-bottom: .25rem; }
.anchor.a-mid { border-left-color: var(--accent); }

.scale { display: flex; flex-direction: column; gap: .85rem; }
.scale-band { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.scale-label {
  font: 600 .6875rem/1.4 var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-faint); min-width: 11rem;
}
.scale-chips { display: flex; gap: .4rem; flex-wrap: wrap; }
.chip { position: relative; }
.chip input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.chip span {
  display: grid; place-items: center; min-width: 2.25rem; height: 2.25rem;
  border: 1px solid var(--rule); border-radius: var(--radius);
  font: 500 .875rem/1 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--ink-soft); background: var(--surface);
}
.chip input:checked + span {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}
