
/* Every box measures the way this sheet's geometry already assumes it does.
   Two rules below compute against a box's outer edge: `.choice` is `width:100%`
   carrying padding and a border, and `.stem p.colmap` insets a matching
   question's header by `--choice-pad + --choice-bd + --choice-mark +
   --choice-gap` so its columns land on the choice bodies underneath it. Both
   were written while a choice was a `<button>`, which the UA sheet gives
   `border-box` for free. When it became a `<div role="radio">` that went away —
   a div is `content-box` — and the padding and border moved outside the 100%:
   measured, every one of 200 choices overflowed its own `<li>` by 30px, the
   colmap header fell 30px out of register on the right while its left edge
   still matched, and the document grew a horizontal scrollbar at phone width.
   Declaring it makes the assumption this sheet's own, rather than a property
   of whichever element a component happens to be built out of. */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;background:var(--ground);color:var(--ink);
  /* The app's default register, for anything that states no size of its own.
     Split out of the `font` shorthand because the shorthand cannot be read at
     a glance once three of its four parts are tokens. The question sheet's own
     reading size is larger and set on `.stem p` and `.choices`; this is the
     chrome's. */
  font-family:var(--font-ui);font-size:var(--text-ui);line-height:var(--lh-ui);
  /* Korean breaks at ANY syllable by default, so without this the browser
     splits words mid-word in every paragraph — 경우 as 경/우, 용인하는 as
     용인하/는. It is the single largest readability defect this sheet can fix,
     and `break-word` is its necessary companion: keep-all alone lets a long
     unbreakable token (a URL, a Latin term) overflow the column. */
  word-break:keep-all;overflow-wrap:break-word}
/* `100vh` on a phone is the viewport with the address bar *hidden*, so the
   column is taller than the screen until the bar collapses and the page
   jumps as it does. `100dvh` is the viewport as it is right now. The `vh`
   line stays underneath as the fallback for anything that does not know the
   dynamic units. */
/* The side padding is `--space-6` plus whatever the display takes. In
   landscape on a notched phone the inset is real — 44px or more — and
   without this the first character of every line sits under the camera
   housing. Portrait and on a desktop the inset is zero and this is exactly
   `--space-6`, which is what `nocap_style::COLUMN_PAD_PX` and every figure's
   `sizes` attribute are computed from. */
main{max-width:720px;margin:0 auto;background:var(--surface);
  padding:var(--space-7)
          calc(var(--gutter) + env(safe-area-inset-right,0px))
          var(--space-16)
          calc(var(--gutter) + env(safe-area-inset-left,0px));
  min-height:100vh;min-height:100dvh}
.papernote{font-size:var(--text-meta);background:var(--warn-soft);border-left:3px solid var(--warn);padding:var(--space-3);margin:0 0 var(--space-4);border-radius:0 var(--radius-sm) var(--radius-sm) 0}
.group{border:1px solid var(--accent-line);border-radius:var(--radius);background:var(--accent-soft);padding:var(--space-3) var(--space-4);margin:0 0 var(--space-4)}
.group p{margin:0 0 var(--space-3)}
/* The reading register. Stem and choices are one size, because in the app a
   choice is already a bordered row on its own surface and needs nothing from
   type to hold it apart from the question. The 1px step that used to do that
   work — stem 17, choice 16 — was paying for the dump preview, where
   `--choice-bd` is 0 and choices genuinely do run together; it charged every
   page for a problem only one of them has. */
.stem p{margin:0 0 var(--space-4);font-size:var(--text-read);line-height:var(--lh-read)}
aside.boxed{border:1px solid var(--line);border-radius:var(--radius);padding:var(--space-3) var(--space-4);margin:var(--space-3) 0;background:var(--surface-2)}
aside.boxed p{margin:0 0 var(--space-2)}
/* A frame is a 보기 box whose border the paper drew with empty table cells.
   The title becomes a heading bar and the content gets an inset body, so a
   figure or a nested table inside it lines up with the prose instead of
   running to the border. */
aside.framed{border:1px solid var(--line);border-radius:var(--radius);margin:var(--space-3) 0;background:var(--surface);overflow:hidden}
.frame-title{margin:0;padding:var(--space-3) var(--space-4);background:var(--surface-3);border-bottom:1px solid var(--line-2);
  font-size:var(--text-meta);font-weight:var(--fw-semi);color:var(--ink-2)}
.frame-body{padding:var(--space-4)}
.frame-body>p:first-child{margin-top:0}
.frame-body>p:last-child{margin-bottom:0}
.frame-body>aside.boxed:first-child{margin-top:0}
/* `○` is a fullwidth glyph — the same 13.8x28px box as a Hangul syllable at
   17px — so at full size it reads as a bullet twice the weight of its text.
   The renderer wraps the one that opens a line; this is the only reason it is
   wrapped. */
.bul{font-size:.72em;vertical-align:.12em;margin-right:.2em;color:var(--muted)}
/* A fraction the paper drew as two stacked table cells, with the row border
   for a bar. Rendered as a grid it put `1` and `2` in bordered boxes and left
   the `배` that belongs to them outside.

   `inline-block` with `vertical-align:middle`, not `inline-flex`: a flex box
   takes its baseline from its first item, so the `배` that follows the
   fraction lined up with the numerator and sat at the top of it.

   The line box has to clear Hangul, not Latin. At 1.18 the numerator and
   denominator are each shorter than the glyphs they hold — `현원` over `정원`
   in 자료해석's 배치율 note ran into each other — because a Korean face spends
   more of its em on ascent and descent than the ratio allows for. 1.32 is the
   first value that clears them, measured against the tallest pair in the
   corpus. */
.frac{display:inline-block;vertical-align:middle;text-align:center;margin:0 .18em;
  font-size:.88em;line-height:1.32}
.frac .num{display:block;border-bottom:1px solid currentColor;padding:0 .28em}
.frac .den{display:block;padding:0 .28em}
.frac p{margin:0;display:inline}
/* A gap in a sentence that the author drew a box around: `(가)` to be filled
   in, `->` between the boxes of a traversal, or nothing at all. 168 of them,
   and as callouts they were block elements that broke the sentence in half. */
.blank{display:inline-block;border:1px solid var(--ink-2);border-radius:var(--radius-xs);padding:var(--space-hair) var(--space-2);
  margin:0 .15em;min-width:1.2em;text-align:center;line-height:1.3}
/* The same gap, where the paper set it on a line of its own rather than inside
   a sentence — the title bar of the web page 2025 9급 영어 6 asks you to name.
   Full width and centred, which is how it is drawn, and a block so the
   passage below starts where the paper starts it. */
.blankline{border:1px solid var(--ink-2);border-radius:var(--radius-xs);padding:var(--space-3);margin:0 0 var(--space-4);
  text-align:center}
/* A row of small frames the paper set side by side. HWP has no inline table,
   so the author drew one frame per chip and let the text flow place them; the
   flow is lost and without this they stack, each as wide as the passage.
   Borderless, because a frame used only to place text is not a drawn box. */
/* Named for the paper, like `papernote`, because `chip` is the app's: it
   dresses one as a filled pill for an exam's grade, and this one is a marker
   the paper set in a small frame beside its neighbours. Sharing the name meant
   the paper's markers silently wore the app's pill. */
.paperchips{display:flex;flex-wrap:wrap;gap:var(--space-1) var(--space-7);margin:0 0 var(--space-3)}
.paperchip{display:inline-block}
/* A listing, where the indentation is the content: 자료구조론 3 asks for the
   complexity of three nested loops and `<p>` collapses every space that shows
   the nesting. `pre-wrap`, not `pre`: a long line must wrap on a phone rather
   than push the page sideways. No colouring — exam code is routinely broken on
   purpose, and highlighting asserts a well-formedness the question is denying. */
pre.code{margin:var(--space-3) 0;padding:var(--space-4);background:var(--surface-2);border:1px solid var(--line);
  border-radius:var(--radius);font-family:var(--font-mono);font-size:var(--text-meta);line-height:var(--lh-ui);
  white-space:pre-wrap;overflow-x:auto;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;tab-size:4}
/* A listing that is the whole callout does not need a second border inside the
   first one. The 보기 box is already the container. */
aside.boxed>pre.code:only-child,.frame-body>pre.code:only-child{margin:0;border:0;background:none;padding:0}
/* Choices read at the stem's size. The shell sets `font:inherit` on `.choice`,
   so the register has to be declared on the list: from there it reaches the
   row, the mark and the body alike. A table inside a choice still sets its
   own. */
.choices{list-style:none;margin:var(--space-5) 0 0;padding:0;
  font-size:var(--text-read);line-height:var(--lh-read)}
/* 8px gave roughly a 36px touch target against a 44px guideline. */
.choice{display:flex;gap:var(--choice-gap);padding:var(--space-4) var(--choice-pad);border-radius:var(--radius-md)}
/* A matching question: `(가) (나) (다)` over the answers that go under them.
   The paper prints columns; the source separates the markers with one space
   and the corpus contains no tabs at all, so there is no spacing to preserve —
   what aligns them is the count. Equal `1fr` columns in both rows line up
   whatever the words are — but only if the two grids are the same box, so the
   header is inset by every part of a choice row that flanks its body: the row's
   padding and border, the number, and the gap after it. Each one is a variable
   because measuring the rendered page found an approximation of them drifting
   1px at the first column and 10px by the third. `--choice-bd` is 0 here, where
   a choice is plain markup; the shell that gives it a border raises it
   there. And rem, not em: it is the width of a column, not of the text beside
   it, so it must not move when some enclosing size does. */
/* Every colour this sheet uses, named once.
   There were 21 literals in here and 26 more in the shell, no two of which
   agreed on what "a border" or "a muted grey" was, and no dark theme anywhere.
   Components below read tokens and never literals, which is what lets the
   second theme be a redefinition rather than a second stylesheet.
   `color-scheme` is not decoration: it is what makes the scrollbar, the
   caret and a form control follow the page instead of staying light. */
:root{
  color-scheme:light dark;
  --ground:#F3F5FA; --surface:#FFFFFF; --surface-2:#FAFBFD; --surface-3:#F1F4FA;
  /* --muted was #79809A: 3.91:1 on white, and it carries the label and the
     sub-line of every tile on the home screen plus the filter's own
     caption. 5.71:1 now. */
  --ink:#0C1020; --ink-2:#414863; --muted:#5E667E;
  --line:#E3E7F0; --line-2:#EDF0F6; --line-strong:#D4DAE7;
  /* How far a note hangs its wrapped lines. Each note overrides this inline
     with its own marker's width; the base value is what a note would use if
     it ever arrived without one, and is here because every token is declared
     at the base level so none can paint nothing in some theme. */
  /* #2563EB, not #2F6BFF. The old one measured 4.50:1 on white — WCAG AA
     wants 4.5 and Lighthouse rounds to 4.49, so it failed by a hundredth,
     on `.iconbtn`'s labels and on white text inside `.fchip`. This is the
     same cobalt a step deeper: 5.17:1, and the hue barely moves. */
  --accent:#2563EB; --accent-ink:#FFFFFF; --accent-soft:#EDF3FF; --accent-line:#C4D6FF;
  --ok:#0E9F63; --ok-soft:#E4F6EE; --ok-line:#A8DCC4;
  --bad:#D63B35; --bad-soft:#FDECEB; --bad-line:#F0B8B5; --bad-ink:#8A2B2B;
  --warn:#C98A12; --warn-soft:#FFF7E8; --warn-line:#EBCF95;
  /* Scanned figures carry their own white ground. Transparent is right on a
     light page; on a dark one an unbacked figure glares, so it gets a plate. */
  --figure-bg:transparent;
  --radius-xs:2px; --radius-sm:4px; --radius:6px; --radius-md:8px; --radius-lg:12px;
  --radius-pill:999px; --radius-round:50%;
  /* The scales. Twenty-four font sizes, twenty-five spacing values and
     nineteen radii were each a component's own decision, made against its
     neighbour rather than against a system. The half-pixel sizes are the tell
     — 9.5, 10.5, 11.5, 12.5, 13.5, 14.5 — because nobody designs a 14.5px
     step, they nudge one component until it looks right, six separate times.
     Every size carries its leading, since a size chosen without one is half a
     decision. Spacing is a 4px grid rounded down at every tie: a reading
     instrument for data papers wants its rows close, not airy. */
  --text-micro:11px;   --lh-micro:1.45;
  --text-meta:13px;    --lh-meta:1.5;
  --text-ui:15px;      --lh-ui:1.6;
  --text-read:17px;    --lh-read:1.75;
  --text-title:21px;   --lh-title:1.3;
  --text-hero:26px;    --lh-hero:1.2;
  --text-display:48px; --lh-display:1.1;
  /* For a box whose height is the glyph itself: a mark, an icon, a key cap. */
  --lh-flat:1;
  --fw-regular:400; --fw-medium:500; --fw-semi:600;
  /* Reserved for display sizes, `--text-title` (21px) and up. Below that a
     700 muddies Hangul counters at UI scale, which is why the rest of the
     app tops out at 600; at 26px and 48px it is the weight the figure
     wants. Five sites use it. */
  --fw-bold:700;
  /* The column's own side padding. Named because things bleed past it —
     the sticky nav draws its rule 4px from the screen edge — and a bleed
     written as a number breaks the first time the padding changes. It
     did: dropping to 12px under 360px left the nav 8px wider than the
     viewport, which the 320px measurement caught. */
  --gutter:var(--space-6);
  --space-hair:1px; --space-1:2px;  --space-2:4px;  --space-3:8px;  --space-4:12px;
  --space-5:16px;   --space-6:20px; --space-7:24px; --space-8:32px; --space-16:64px;
  --font-ui:-apple-system,'Apple SD Gothic Neo',system-ui,'Segoe UI','Malgun Gothic','Noto Sans KR',sans-serif;
  --font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,'D2Coding','Noto Sans Mono CJK KR',monospace;
  --choice-pad:var(--space-4); --choice-gap:var(--space-4); --choice-mark:1.4rem;
  --choice-bd:0px;
  /* The mark's colour, declared here at the value the borderless dump needs
     and raised by the shell, exactly as `--choice-bd` is. In a preview the
     mark is the only thing distinguishing one choice from the next, so it
     carries the accent; in the app the bordered row does that work and a
     coloured numeral would just be noise. */
  --choice-mark-ink:var(--accent);
  /* A grid cell's own vertical padding. Named because a marker beside a table
     has to clear exactly as much of it to sit on the table's first line. */
  --cell-pad-y:var(--space-3);
}
/* Under 360px the column gives a fifth of its width to its own margins.
   A 320px screen keeps 296px of reading measure this way instead of 280.
   Everything that bleeds past the column follows automatically, because
   they are all written in terms of `--gutter`.

   **After the `:root` it overrides, not before.** Same specificity means
   source order decides, and the first version of this sat beside `main`
   two hundred lines above the declaration — so it lost, silently, and
   320px kept the wide padding. Measuring is what found it; nothing about
   the rule looks wrong. */
@media (max-width:359px){ :root{--gutter:var(--space-4)} }
/* Only the tokens are redefined. No component rule lives in here — one that
   did would never apply to a viewer whose system is light, and the page would
   render one theme's text on the other theme's ground. */
@media (prefers-color-scheme:dark){
  :root{
    --ground:#080A10; --surface:#121620; --surface-2:#171C28; --surface-3:#1B2130;
    --ink:#E9EDF7; --ink-2:#AEB6CC; --muted:#79829B;
    --line:#242B3A; --line-2:#1C2230; --line-strong:#313A4D;
    --accent:#5C8BFF; --accent-ink:#06122E; --accent-soft:#16233F; --accent-line:#2B3F6E;
    --ok:#3BD48D; --ok-soft:#10301F; --ok-line:#1E5B3C;
    --bad:#FF6B63; --bad-soft:#331513; --bad-line:#5E2B27; --bad-ink:#FFC9C5;
    --warn:#E0B34D; --warn-soft:#2B2210; --warn-line:#544425;
    --figure-bg:#FFFFFF;
  }
}
.colmap{display:grid;grid-template-columns:repeat(var(--n),minmax(0,1fr));gap:0 10px;align-items:baseline;
  overflow-x:auto;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch}
.stem p.colmap{margin:var(--space-4) 0 0;
  padding-left:calc(var(--choice-pad) + var(--choice-bd) + var(--choice-mark) + var(--choice-gap));
  padding-right:calc(var(--choice-pad) + var(--choice-bd))}
.cols-2{--n:2}.cols-3{--n:3}.cols-4{--n:4}.cols-5{--n:5}.cols-6{--n:6}.cols-7{--n:7}
.cbody{min-width:0;flex:1}.cbody p{margin:0}
/* A choice the author lined up with spaces rather than a tab. An accounting
   entry sets its two sides against each other — `(차) …   (대) …` — and HTML
   collapsing that padding is what slid `(대)` to a different place in every
   choice. `pre-wrap`, not `pre`: the padding is kept and the line still wraps
   at the edge of the column on a narrow screen. */
.cbody.padded{white-space:pre-wrap}
/* A line the author split into columns with a tab. The same instrument as the
   choice above and for the same reason: HTML collapses a tab exactly as it
   collapses a space, so without this `ㄱ.  어린이공원` and `ㄴ.  역사공원` —
   a tab stop apart in 조경학 2 — arrive as one run of prose. `tab-size` is the
   code block's 4, because these are the paper's own stops and four is what it
   sets them at. Only a tab *between* two things earns this: a leading one is
   an indent and is left to collapse as it always has. */
.tabrow{white-space:pre-wrap;tab-size:4}
/* A 보기 item whose label and body are two nodes: `ㄱ.` and the grid it
   introduces. Side by side, because that is how the paper sets them and
   because a label alone on a line reads as a heading for everything below it
   rather than a name for the one block beside it. `align-items:start` keeps
   the label on the block's first line however tall the block turns out. */
.labelled{display:flex;gap:.45em;align-items:flex-start;margin:0 0 var(--space-4)}
.labelled>.lbl{flex:0 0 auto}
.labelled>.lbody{min-width:0;flex:1}
.labelled>.lbody>:last-child{margin-bottom:0}
.mark{flex:0 0 var(--choice-mark);color:var(--choice-mark-ink)}
/* The preview's only separator. Choices sit flush against each other, so in
   the app the bordered row already parts them and `--choice-bd` is 1px —
   this resolves to zero width and draws nothing. In the dump the border is
   0, and it resolves to a hairline, which is what stops two choices reading
   as one paragraph now that they are also one size. */
.choices li + li{border-top:calc(var(--space-hair) - var(--choice-bd)) solid var(--line)}
/* `overscroll-behavior-x:contain` is the load-bearing one: without it,
   reaching the end of a wide table hands the gesture to the browser, and on
   iOS a horizontal swipe at the edge of the viewport is Back. A candidate
   scrolling to the last column of a 자료해석 table was one flick away from
   losing the page. `-webkit-overflow-scrolling` is for iOS before 13, where
   momentum was opt-in; it is inert everywhere newer and costs nothing. */
.tablewrap{overflow-x:auto;overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;
  margin:var(--space-3) 0}
/* A table that opens its container has nothing above to be separated from, and
   the margin only pushes it away from the marker naming it. */
.cbody > .tablewrap:first-child, .lbody > .tablewrap:first-child{margin-top:0}
table.grid{border-collapse:collapse;font-size:var(--text-meta);min-width:100%}
/* A cell's paragraph belongs to the cell, not to the stem. `.stem p` matches a
   `<p>` inside a `<td>` directly, and a directly matching rule beats the
   table's own size arriving by inheritance — so the 14px above governed 414 of
   this paper's 2538 cells while the other 2124 rendered at 17px, the same
   markup reading one size in a stem and another in a choice. The margin was
   the worse half: `.stem p`'s 12px trailing gap is prose spacing, and it landed
   under 2224 cell paragraphs that never asked for it, making a single-line row
   58.8px in a stem against 41.5px in a choice. `.cbody p` already takes the
   margin back for choices; this does it for every cell, and hands the leading
   to the table too, because a dense grid is not read at prose leading. */
table.grid p{font-size:inherit;line-height:var(--lh-meta);margin:0}
/* Left, not centre. These cells hold prose: measured across the corpus, the
   median is 55 characters and 77% run past 30. Centring a sentence is the
   defect; centring a short value was only ever incidental. */
/* Centred, because that is what HWP does and what the papers do: of the
   corpus's 27,285 table cells, 26,501 are centred vertically, 744 sit at the
   top and 40 at the bottom. The stylesheet takes the 97% and `va-t`/`va-b`
   carry the 784 that say otherwise, the same bargain the hidden edges make. */
table.grid td{border:1px solid var(--line-strong);padding:var(--cell-pad-y) var(--space-3);text-align:left;vertical-align:middle}
table.grid td.va-t{vertical-align:top}
table.grid td.va-b{vertical-align:bottom}
table.grid.nowrap td{white-space:nowrap}
/* The rule a header corner draws across itself, naming both of the table's
   axes at once: 「연도」 above the line and 「재원」 below it. 191 cells in 250
   papers draw one and all but a single cell draw `\`.

   A background gradient, not a border: the line runs corner to corner at
   whatever height the cell settles to, and no border property draws that. A
   corner keyword tracks the box as it resizes, which a fixed angle would not.
   The two 0.5px stops are what keep it a hairline rather than a wedge — a
   single stop antialiases into a soft band.

   **The keyword names the gradient's axis, not the line.** The bands run
   *perpendicular* to it, so `to top right` draws `\` and `to bottom right`
   draws `/` — the opposite of how they read. Written the other way round
   first, which put the rule across the wrong pair of corners and left 「구분」
   and 「일자」 both sitting on it. Measured in a browser rather than reasoned
   about: sample the painted gradient at the four corners and the one that is
   `\` lights up top-left and bottom-right. */
table.grid td.dg-b{background-image:linear-gradient(to top right,
  transparent calc(50% - 0.5px),var(--line-strong) calc(50% - 0.5px),
  var(--line-strong) calc(50% + 0.5px),transparent calc(50% + 0.5px))}
table.grid td.dg-s{background-image:linear-gradient(to bottom right,
  transparent calc(50% - 0.5px),var(--line-strong) calc(50% - 0.5px),
  var(--line-strong) calc(50% + 0.5px),transparent calc(50% + 0.5px))}
table.grid td.dg-x{background-image:
  linear-gradient(to top right,transparent calc(50% - 0.5px),var(--line-strong) calc(50% - 0.5px),var(--line-strong) calc(50% + 0.5px),transparent calc(50% + 0.5px)),
  linear-gradient(to bottom right,transparent calc(50% - 0.5px),var(--line-strong) calc(50% - 0.5px),var(--line-strong) calc(50% + 0.5px),transparent calc(50% + 0.5px))}
/* 데이터베이스론 draws a transaction schedule's time axis as a line object in a
   tall empty cell beside the rows, with an arrowhead at the foot and 「시간」
   over it. Without it there is nothing on the page saying which way time runs,
   and the question cannot be answered — so it is semantic, not decoration.

   A background gradient for the shaft, the same trick as the diagonals: it has
   to reach the full height the rows come to, which no fixed length knows. The
   head is a border triangle on `::after`, centred on the shaft and sat on the
   bottom edge. Both are painted on the cell, so neither can disturb the text
   flow of a cell that turns out to hold words as well. */
table.grid td.ax-d{position:relative;background-image:linear-gradient(to right,
  transparent calc(50% - 0.5px),var(--line-strong) calc(50% - 0.5px),
  var(--line-strong) calc(50% + 0.5px),transparent calc(50% + 0.5px))}
table.grid td.ax-d::after{content:"";position:absolute;left:50%;bottom:2px;
  transform:translateX(-50%);width:0;height:0;
  border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:7px solid var(--line-strong)}

/* The two labels sit either side of the rule the way the paper sets them: the
   first pushed right, above the line, the last pushed left, below it. */
table.grid td[class*="dg-"] > p:first-child{text-align:right}
table.grid td[class*="dg-"] > p:last-child{text-align:left}
/* The edges the paper does not draw. Authors lay a diagram out with a table
   and switch individual borders off — an array whose index row is unboxed, a
   hash table with only its slots ruled — and a blanket grid turns the diagram
   into a spreadsheet. 620 of the corpus's 2,973 cells hide an edge.

   `border-collapse` gives a shared edge to whichever side draws it, which is
   what HWP does: the line shows if either cell asks for it. */
table.grid td.nb-l{border-left:0}
table.grid td.nb-r{border-right:0}
table.grid td.nb-t{border-top:0}
table.grid td.nb-b{border-bottom:0}
/* There is deliberately no `td p{display:inline}` rule here.
   One existed as a stopgap while the renderer still emitted
   `<p><math display="block">1</math></p><p>시간 미만</p>` for a single cell, and
   it flattened every paragraph in every cell — including a 공문서 box whose
   seven paragraphs are the content. `coalesce_splices` now joins those
   fragments in the markup, so the cell arrives as one `<p>` and the hack is
   both unnecessary and destructive. Cells that hold real paragraphs keep
   them. */
img.figure{height:auto;display:block;margin:var(--space-3) 0;background:var(--figure-bg);border-radius:var(--radius);max-width:100%}
/* A diagram fills the reading column. The paper's own display width is the
   ratio, not the cap: 608 of the corpus's 794 figures are 350px or wider in a
   680px column, so honouring the printed width left every diagram at about
   62% of the text measure beside it. The other 186 — symbols and formula
   strips, 164 of them under 40px — keep theirs. `crates/render`'s
   FIGURE_FILLS_COLUMN is the cut. */
img.figure.wide{width:100%}
/* A picture the size of a letter, sitting in a line of words.
   데이터베이스론 13 writes the join `R ⋈ S` and HWP stores the ⋈ as a 15x12px
   image rather than a character, so the reader hands it over as a picture and
   `img.figure` — `display:block`, with a tint and a radius — put every one on
   a line of its own inside a grey box. Both halves of that report, one cause.
   Inline, no chrome, and nudged onto the baseline. */
img.sym{display:inline-block;vertical-align:-0.12em;margin:0;background:none;
  border-radius:0;max-width:100%;height:auto}
/* A figure with the paper's own words laid back over it. Hancom writes a
   caption onto a screenshot by floating a 글상자 above it rather than drawing
   the words into the raster, so the words survive as text and the placement
   survives as geometry — 2026 9급 영어 6-7 puts a browser tab name and address
   bar on one this way, and the two 9급 영어 papers hold five between them.

   The wrapper is the picture's box and nothing wider: `aspect-ratio` holds it
   before the bytes land, so the labels do not jump when they do.
   `container-type:inline-size` is what makes `cqw` on a label mean a share of
   the figure rather than of the viewport — that is how a 9pt caption grows
   with the screenshot when the column stretches it, and it is the whole
   mechanism. Without it the labels stay put and the picture moves out from
   under them.

   `overflow:visible` on purpose: a text box in HWP is sized to its first line
   and the address bars overrun theirs. Clipping would cut the URL that the
   question asks about. */
span.overlaid{position:relative;display:block;container-type:inline-size;margin:var(--space-3) 0;overflow:visible}
span.overlaid>img.figure{margin:0;width:100%}
/* Ink, not `--ink`. These words sit on a screenshot of a browser or a mail
   client, and that raster stays bright in dark mode because it is a picture
   rather than a styled element. Following the theme turned the tab name and
   the address bar near-white on a white address bar and they vanished. The
   picture cannot follow the theme, so the words on it must not either. */
span.overlaid>.ol{position:absolute;line-height:1.15;white-space:nowrap;color:#0C1020}
/* Paragraph alignment, carried from the document. Two classes, not six: HWP's
   default is Justify and its Left is what a browser does anyway, so only
   centre and right are written at all — 25,464 and 3,337 paragraphs, 98% of
   them table cells that the printed paper centres and this rendered flush
   left. `text-align` alone is enough because a `<p>` already fills its cell. */
/* A note hangs under its own text, and the marker's width is the browser's to
   measure. Held apart as two flex items rather than bought with
   `padding-left`/`text-indent`: every width this code could compute for
   `※  ` or `○  ` is a guess about a glyph it cannot see, and the guess it
   used to make — four `ch`, 42.84px — was more than twice the 19.2px the
   marker actually paints. `white-space:pre` on the marker keeps the spaces
   the paper put after it, which are part of the gap. */
p.fnote{display:flex;align-items:baseline}

p.fnote>.mk{flex:0 0 auto;white-space:pre}
/* The column a run of numbered notes hangs in. It holds the symbol that opened
   the run — `※` and the spaces after it — so the browser measures the very
   glyphs that set the column, and hides it, so `2)` and `3)` sit under `1)`
   rather than flush against the margin. `visibility:hidden` and not `display:
   none`: the width is the whole point of it being there. */
p.fnote>.gut{flex:0 0 auto;white-space:pre;visibility:hidden}
p.fnote>.nb{flex:1 1 auto;min-width:0}
p.ctr{text-align:center}
p.rgt{text-align:right}
/* Which exam the bars below belong to. One line above the list rather than a
   chip on each row: the name is the same fifty times, and the row's job is the
   subject and the score. */
p.barsof{margin:var(--space-5) 0 var(--space-2);font-size:var(--text-meta);color:var(--muted)}
/* MathML, rendered natively — the corpus already carries <math> elements and
   every current browser draws them. No KaTeX, no MathJax: injecting one would
   mean converting MathML backwards to re-render what already works. What was
   missing is only a math font and somewhere for a wide equation to go.
   `math` is the CSS generic family for mathematical text. */
math{font-family:'STIX Two Math','Latin Modern Math','Cambria Math',math}
math[display="block"]{display:block;margin:var(--space-3) 0;overflow-x:auto;
  overscroll-behavior-x:contain;-webkit-overflow-scrolling:touch;max-width:100%}
/* Matrix columns align on the right so a minus sign cannot push its digits out
   of line. MathML centres `mtd` by default, which sets `-4` and `2` on
   different axes and makes an even grid look ragged — the same reason LaTeX
   numeric arrays use `r` columns.
   `cases` is untouched: pulldown-latex marks it `env-cells-left`, and a
   piecewise condition reads left. */
math mtable.env-arraylike>mtr>mtd{text-align:right}
/* Inline math sets a fraction's numerator and denominator at script size —
   at 14px that is 10px, which is what made 13/2 hard to read in a choice. A
   modest bump rather than `math-style:normal`, which would set every inline
   fraction at full display size and push the line boxes around it apart.
   638 fractions in the corpus; 5 sit inside another, and the second rule
   leaves those smaller than their parent, which is the distinction the
   nesting draws. */
math[display="inline"] mfrac{font-size:1.15em}
math[display="inline"] mfrac mfrac{font-size:1em}
.math-fallback{background:var(--bad-soft);border:1px solid var(--bad-line);border-radius:var(--radius-sm);padding:0 var(--space-2);font-size:var(--text-meta);color:var(--bad-ink);cursor:help}
nav{font-size:var(--text-meta);margin-bottom:var(--space-5)}

a{color:var(--accent)}
/* Spoken, never shown. Not `display:none` and not `visibility:hidden` —
   both take the element out of the accessibility tree, which is the one
   thing this must stay in. A 1px clipped box is the shape that survives
   every screen reader. */
.sronly{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
/* ── a phone held in one hand ─────────────────────────────────────────
   Inherited, so one declaration covers every tappable thing on the site and
   the grey flash on a tap is gone everywhere rather than on the three
   selectors that had thought to ask. */
html{-webkit-tap-highlight-color:transparent;
  /* Pull-to-refresh costs a candidate every selection on the page, and the
     gesture that triggers it — a short drag — is the gesture for scrolling a
     list of choices. The elastic bounce goes with it, which in a standalone
     window is what you want anyway: there is no browser chrome for it to
     bounce against. */
  overscroll-behavior-y:none}
/* Removes the double-tap-to-zoom wait on things that are meant to be tapped
   once. Deliberately NOT global: a 자료해석 table arrives as a figure, and
   double-tap to zoom it is the only way to read one on a phone. Pinch is
   unaffected either way. */
a,button,summary,.choice{touch-action:manipulation}
/* 16px is the threshold below which iOS Safari zooms the page to meet the
   caret on focus — and then does not zoom back out, leaving a candidate on a
   magnified page with no obvious way off it. The body reads at 15px by
   choice; a field a thumb types into is the one place that costs more than
   it saves. */
input,select,textarea{font-size:16px}
/* A choice is a `div[role=radio]` and draws no focus ring of its own, so
   keyboard users had nothing to go on — and even as a button, what the browser
   drew against a custom background was often nothing. `:focus-visible` rather than `:focus` so a mouse click does not leave
   a ring behind. */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius)}
.choice:focus-visible{outline-offset:-2px}
header{display:flex;align-items:baseline;gap:var(--space-3);flex-wrap:wrap;margin:0 0 var(--space-6)}
/* ── the home grid ────────────────────────────────────────────────────
   Two columns at every width. A phone fits two of these comfortably and one
   of them wastes the screen; a desktop gets the same grid inside the same
   720px column, because this is a page people open on a train and the
   desktop view is the rarer one. */
.bento{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin:0 0 var(--space-7)}
.tile{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);
  padding:var(--space-4) var(--space-4);color:var(--ink);text-decoration:none}
.tile .k{font-size:var(--text-micro);font-weight:var(--fw-semi);letter-spacing:.06em;color:var(--muted)}
.tile .v{font-size:var(--text-ui);font-weight:var(--fw-semi);letter-spacing:-.01em;line-height:var(--lh-title)}
.tile .s{font-size:var(--text-meta);color:var(--muted);line-height:var(--lh-micro)}
/* The one filled surface on the page. */
.tile.hero{grid-column:1/-1;background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
.tile.hero .v{font-size:var(--text-title);font-weight:var(--fw-bold)}
/* No opacity. On a saturated accent the AA floor is a luminance of 0.864
   out of white's 1.000 — the top fourteen per cent of the range — so every
   softening you can actually see fails, and the one that passes (.94) is a
   softening nobody can see. The hierarchy here was never carried by the
   opacity anyway: `.k` is 11px semibold and `.s` is 13px against a 24px
   bold value. 4.01:1 before, 5.17:1 now. */
.tile.hero .k,.tile.hero .s{color:var(--accent-ink)}
/* Done for today. A solid white pill with accent text rather than a
   translucent one: white on a tint of the accent is lighter than white on
   the accent, so the softer-looking version is the one that fails AA. This
   way is 5.17:1, the same as every other word on the tile. */
.tick{display:inline-flex;align-items:center;gap:3px;margin-left:var(--space-2);
  padding:1px var(--space-2);border-radius:var(--radius-pill);
  background:var(--accent-ink);color:var(--accent);
  font-size:var(--text-micro);font-weight:var(--fw-bold);letter-spacing:0;
  vertical-align:middle}
.tick svg{width:9px;height:9px;flex:0 0 auto}
.tile.due{background:var(--accent-soft);border-color:var(--accent-line)}
.tile.due .k,.tile.due .v{color:var(--accent)}
/* A tile standing on its own rather than in the bento. On `/` the grid's gap
   does this; on `/me` a card sat hard against the heading of the list under
   it, and two sections with nothing between them read as one. Scoped to a
   direct child of `main`, which is the only place a loose tile occurs. */
main > .tile{margin:0 0 var(--space-6)}
/* ── the scope chooser ────────────────────────────────────────────────
   Directly under the tiles it changes, because a filter you cannot see beside
   its effect is the one people forget they set. Buttons in a form, not links:
   this writes a cookie, and a prefetcher following a link would change what
   somebody sees without them asking. */
/* Nothing of its own left but the space under it: the pills, the label and
   the pressed state all come from `.frow`/`.flabel`/`.fchip` further down, so
   the three places this app asks "which slice?" answer in one visual language.
   A control that looks like nothing else on the site is a control people ask
   why it does not open. */
.scopepick{margin:0 0 var(--space-7)}
/* ── the paper index ──────────────────────────────────────────────────
   Still every paper, still one tap each, but as a two-column list of rows
   rather than 80 underlined links down a page. The count is the only number,
   so it is the only thing set in tabular figures. */
.exam{margin:0 0 var(--space-7)}
.examhead{font-size:var(--text-meta);font-weight:var(--fw-semi);letter-spacing:-.01em;margin:0 0 var(--space-3);
  display:flex;align-items:baseline;gap:var(--space-3);flex-wrap:wrap}
.papers{list-style:none;margin:0;padding:0;
  display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:var(--space-hair)}
.papers a{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-3) var(--space-3);border-radius:var(--radius);color:var(--ink);text-decoration:none;font-size:var(--text-meta)}
.papers .sub{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.papers .n{font-size:var(--text-meta);color:var(--muted);font-variant-numeric:tabular-nums;flex:0 0 auto}
/* ── the paper index ──────────────────────────────────────────────────
   Its own page now. The heading leads with the grade, because that is what a
   candidate picks by, and carries the year beside it so ten years of one exam
   are told apart at a glance rather than by reading titles. */
.filter{width:100%;font:inherit;font-size:16px;padding:var(--space-4) var(--space-4);margin:0 0 var(--space-6);
  background:var(--surface);color:var(--ink);
  border:1px solid var(--line);border-radius:var(--radius-lg)}
.filter::placeholder{color:var(--muted)}
.filter:focus-visible{border-color:var(--accent)}
.chip{font-size:var(--text-micro);font-weight:var(--fw-semi);letter-spacing:.02em;padding:var(--space-1) var(--space-2);border-radius:var(--radius);
  background:var(--accent-soft);color:var(--accent);flex:0 0 auto}
.examyear{font-size:var(--text-meta);color:var(--muted);font-variant-numeric:tabular-nums;flex:0 0 auto}
/* Hidden by the filter. A class rather than the attribute, because these are
   list items with no author `display` — but being explicit costs nothing and
   survives someone giving `li` a display rule later. */
.papers li.off{display:none}
.exam[hidden]{display:none}
.sit.off{display:none}

/* ── filters ──────────────────────────────────────────────────────────
   Two hundred and forty papers and fifty-five 직류 are both past the size
   where a page can just be a list. What a candidate does with either is not
   "read it" but "get to my bit", and their bit is named by axes the corpus
   already holds: 연도 and 급수.

   Pills in a scrolling row, not a segmented control and not a `<select>`.
   A segmented control is the better shape for two or three fixed options and
   the wrong one the moment there are six — and 연도 gains a value every year,
   so it would have been right this year and cramped by 2029. A `<select>`
   never outgrows anything but hides the options behind a tap and a system
   sheet, which is a poor trade when there are four. A row of pills shows every
   option, stays a 34px tap target, and when it does outgrow the width it
   scrolls sideways instead of reflowing into a second line that pushes the
   list down the screen.

   Sticky under the nav, because the alternative is scrolling back up past a
   hundred rows to change your mind. `--navh` is what it sticks under; it is a
   token rather than a number so the two cannot drift apart. */
.filters{position:sticky;top:var(--navh);z-index:10;margin:0 -16px var(--space-5);
  padding:var(--space-3) var(--space-5) var(--space-1);background:var(--ground);border-bottom:1px solid var(--line)}
.filters .filter{margin:0 0 var(--space-3)}
/* `scrollbar-width:none` and the WebKit twin hide the bar itself, not the
   scrolling: a horizontal scrollbar sitting under four pills reads as a
   rendering fault on a desktop, and on a phone it was never drawn anyway. */
.frow{display:flex;align-items:center;gap:var(--space-2);overflow-x:auto;scrollbar-width:none;
  margin:0 0 var(--space-3);padding:var(--space-hair) var(--space-1) var(--space-1) 0}
.frow::-webkit-scrollbar{display:none}
.frow .flabel{flex:0 0 auto;font-size:var(--text-micro);font-weight:var(--fw-semi);letter-spacing:.06em;
  color:var(--muted);margin:0 var(--space-1) 0 0}
.fchip{flex:0 0 auto;display:inline-flex;align-items:center;min-height:34px;
  padding:0 var(--space-4);border:1px solid var(--line);border-radius:var(--radius-pill);
  background:var(--surface);color:var(--ink-2);font:inherit;font-size:var(--text-meta);
  font-weight:var(--fw-semi);line-height:var(--lh-flat);white-space:nowrap;cursor:pointer;
  -webkit-tap-highlight-color:transparent}
/* The state lives in `aria-pressed`, and the style reads it from there rather
   than from a parallel class. One source of truth means a chip cannot look
   selected to a sighted candidate and unselected to a screen reader. */
.fchip[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);
  color:var(--accent-ink)}

/* ── the set runner ───────────────────────────────────────────────────
   One question at a time, but only once the script says so. Until then
   `.runner` is a plain stack and every question is visible, which is what a
   candidate with JavaScript off still gets. */
.qtop{display:none}
.runner.paged .qtop{display:flex;align-items:center;gap:var(--space-4);margin:0 0 var(--space-5);
  position:sticky;top:env(safe-area-inset-top,0px);z-index:2;
  background:var(--ground);padding:var(--space-3) 0}
.xbtn{font-size:var(--text-read);line-height:var(--lh-flat);color:var(--muted);text-decoration:none;flex:0 0 auto}
.rail{flex:1;height:4px;border-radius:var(--radius-pill);background:var(--line);overflow:hidden}
.rail>span{display:block;height:100%;width:0;background:var(--accent);border-radius:var(--radius-pill);
  transition:width .18s ease}
.qcount{font-size:var(--text-meta);font-weight:var(--fw-semi);color:var(--ink-2);font-variant-numeric:tabular-nums;
  flex:0 0 auto;min-width:3.4em;text-align:right}
.runner.paged .qcard{display:none}
.runner.paged .qcard.on{display:block}
/* Without the script these would be two buttons that do nothing. */
.runnernav{display:none}
.runner.paged .runnernav{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin:var(--space-6) 0 0}
.navbtn{font:inherit;font-weight:var(--fw-semi);padding:var(--space-4);border-radius:var(--radius-lg);cursor:pointer;
  background:var(--surface);color:var(--ink);border:1px solid var(--line)}
.navbtn.next{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.navbtn[disabled]{opacity:.4;cursor:default}
/* A hint for keys that only exist where there is a keyboard. Coarse pointers
   never see it, which is most of this audience most of the time. */
.kbdhint{display:none}
@media (pointer:fine){
  .runner.paged .kbdhint{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center;
    margin:var(--space-5) 0 0;font-size:var(--text-micro);color:var(--muted)}
  .kbdhint b{font-weight:var(--fw-medium);background:var(--surface);border:1px solid var(--line);
    border-bottom-width:2px;border-radius:var(--radius-sm);padding:var(--space-hair) var(--space-2);color:var(--ink-2);
    font-family:var(--font-mono);font-size:var(--text-micro)}
}
/* ── what the ladder did ──────────────────────────────────────────────
   A result, not a reprimand. The miss line is the one that matters: it is the
   moment the free review feature becomes visible, so it reads as the system
   catching the question rather than as a penalty. Both use the semantic
   colours, which are deliberately not the accent. */
.srs{display:flex;align-items:center;gap:var(--space-2);margin:var(--space-4) 0 0;padding:var(--space-3) var(--space-4);
  border-radius:var(--radius-lg);font-size:var(--text-meta);line-height:var(--lh-meta);font-weight:var(--fw-medium)}
/* The same trap `.reveal[hidden]` above exists for: `display:flex` is an author
   rule and `hidden` works through the UA sheet's `[hidden]{display:none}`, so
   without this both outcomes render at once — the page telling a candidate
   they were right and wrong about the same question. */
.srs[hidden]{display:none}
.srs::before{content:"";flex:0 0 6px;height:6px;border-radius:var(--radius-round)}
.srs-ok{background:var(--ok-soft);color:var(--ok);border:1px solid var(--ok-line)}
.srs-ok::before{background:var(--ok)}
.srs-no{background:var(--accent-soft);color:var(--accent);border:1px solid var(--accent-line)}
.srs-no::before{background:var(--accent)}
.srs-out{background:var(--surface-2);color:var(--ink-2);border:1px solid var(--line)}
.srs-out::before{background:var(--muted)}
/* The run of correct answers, in the bar where the progress already is. */
/* ── 잔디 ─────────────────────────────────────────────────────────────
   Ten columns of seven, laid out column-major, so each column is one week and
   the weekdays line up. The cells are square and sized by the grid rather than
   fixed, so the whole thing fits any phone without a media query. */
.grass{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);
  padding:var(--space-4) var(--space-4);margin:0 0 var(--space-7)}
.grass-head{display:flex;justify-content:space-between;align-items:baseline;margin:0 0 var(--space-3)}
.grass-head .k{font-size:var(--text-micro);font-weight:var(--fw-semi);letter-spacing:.06em;color:var(--muted);margin:0}
.grass-head .n{font-size:var(--text-meta);font-weight:var(--fw-semi);color:var(--ink);font-variant-numeric:tabular-nums}
/* A grid of coloured squares is obvious to a developer and means nothing to a
   candidate, which is the whole of the problem this caption and legend fix.
   Saying what fills a square is worth more than any amount of styling. */
.grass-why{font-size:var(--text-micro);color:var(--muted);margin:0 0 var(--space-3);line-height:var(--lh-micro)}
.grass-key{display:flex;align-items:center;justify-content:center;gap:var(--cellgap);
  margin:var(--space-3) 0 0;font-size:var(--text-micro);color:var(--muted)}
/* The legend's tiles are the field's tiles — same size, same soil, same
   blades — so a candidate matches them by looking rather than by inferring.
   They inherit the stroke colours from `.cells i.lN` because both carry the
   same classes; only the sway is left off, since five swatches waving next to
   the sentence explaining them is noise. */
.grass-key i{display:block;width:var(--cell);height:var(--cell);border-radius:var(--radius-xs);
  background:var(--soil);overflow:hidden}
.grass-key i svg{display:block;width:100%;height:100%}
.grass-key i svg path{fill:var(--soil)}
.grass-key i.l1 svg path{fill:var(--grass-1)}
.grass-key i.l2 svg path{fill:var(--grass-2)}
.grass-key i.l3 svg path{fill:var(--grass-3)}
.grass-key i.l4 svg path{fill:var(--grass-4)}
.grass-key i.l5 svg path{fill:var(--grass-4)}
.grass-key i.l5 svg .petal{fill:var(--bloom);stroke:var(--grass-4);stroke-width:.3}
.grass-key i.l5 svg .eye{fill:var(--bloom-eye)}
/* A fixed cell, not `1fr`. Ten columns of `1fr` across a phone made every
   square 36px and the widget 362px tall — a third of the screen for a streak.
   A contribution graph is meant to be read at a glance, not scrolled past.

   `centre`, not `start`. The cell is capped at 26px so the field never walks
   back toward filling the width, which is right — but it left ten weeks of
   grass hard against the left edge of a 690px card with 400px of nothing
   beside it, and a widget with all its air on one side reads as broken rather
   than as restrained. Centring spends the same slack symmetrically, and the
   legend below is centred with it so the two share an axis.

   Not `auto-fit`/`auto-fill`: those size tracks to fill the container, which
   is the 36px squares this cap exists to prevent. The field is a fixed shape
   — seven days by ten weeks — so the question was never how many columns fit,
   only where the fixed shape sits.

   `grid-auto-columns` is still not optional: flowing by column leaves the
   columns implicit and auto-sized, and a cell sized against an auto column is
   asking the column how wide it is while the column asks back. The grid
   renders and every cell is zero wide. */
.cells{display:grid;grid-template-rows:repeat(7,var(--cell));grid-auto-flow:column;
  grid-auto-columns:var(--cell);gap:var(--cellgap);justify-content:center}
/* Blades, not squares. The tile stays as the ground each day is drawn on —
   without it an empty day is a hole rather than bare earth, and seventy holes
   read as damage. */
.cells i{display:block;width:var(--cell);height:var(--cell);border-radius:var(--radius-xs);
  background:var(--soil);overflow:hidden}
.cells i svg{display:block;width:100%;height:100%}
.cells i svg path{fill:var(--soil)}
.cells i.l1 svg path{fill:var(--grass-1)}
.cells i.l2 svg path{fill:var(--grass-2)}
.cells i.l3 svg path{fill:var(--grass-3)}
.cells i.l4 svg path{fill:var(--grass-4)}
/* The top step keeps level 4's green: the reward is the flower, not a sixth
   shade nobody could tell from the fifth. The petals are outlined rather than
   painted white and left alone, because white on `--soil` is a pale smudge in
   light mode and the hairline is what makes it a daisy at 24px. */
.cells i.l5 svg path{fill:var(--grass-4)}
.cells i.l5 svg .petal{fill:var(--bloom);stroke:var(--grass-4);stroke-width:.3}
.cells i.l5 svg .eye{fill:var(--bloom-eye)}

/* Wind.
   Rotation about the base, which is how a blade actually moves — a translate
   would slide the roots. Three degrees, because grass at 16px needs the
   suggestion of movement and not the fact of it: any more and seventy tiles
   become a shimmer nobody can read a streak through.
   Two groups per tile in opposite phase, so a clump bends rather than tilting
   as one rigid object. */
/* Two animations, not one, and that is what stops the field looking wound by
   the same key.

   The clump sways as a unit — that is the gust — and each blade sways again
   within it, slower and further, on its own delay. Composed rather than
   chosen: a single rotation could be staggered per tile *or* per blade, since
   both selectors carry the same specificity and the later one simply wins, and
   the version that staggered per blade made every tile identical while the
   version that staggered per tile made every blade inside one move as a comb.
   Rotating the group and the path is the only way to have both.

   `transform-box:fill-box` is what makes the blade rotation a blade rotation.
   Without it `transform-origin` on an SVG child resolves against the whole
   16-unit viewport, so every blade pivots about the tile's centre-bottom and
   the outer ones scythe sideways instead of bending. With it each blade turns
   about its own root, which is where grass is attached to the ground. */
@keyframes gust{0%,100%{transform:rotate(-2.5deg)}50%{transform:rotate(2.5deg)}}
@keyframes blade{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(5deg)}}
@keyframes bloom{0%,100%{transform:rotate(-4.5deg)}50%{transform:rotate(4deg)}}
.cells i svg g{transform-origin:50% 100%;animation:gust 3.4s ease-in-out infinite}
.cells i svg g.b{animation-duration:4.1s;animation-direction:reverse}
/* The flower nods on a period of its own and a wider arc than the clump. Grass
   at 2.5° is a suggestion; a daisy on a single stem is top-heavy and visibly
   swings, and matching it to the blades around it would have read as one rigid
   object painted with a flower on it. */
.cells i svg g.f{animation:bloom 5.6s ease-in-out infinite}
/* The stem does not flex on top of the nod. Every other path here bends about
   its own root while its clump sways, which is two movements a blade really
   has; a stem that did the same would swing its tip out from under the petals,
   because the head is not a path and cannot follow it. Rigid stem, nodding
   flower, one movement. */
.cells i svg g.f path{animation:none}
.cells i svg path{transform-box:fill-box;transform-origin:50% 100%;
  animation:blade 5.3s ease-in-out infinite}
/* The front of a clump catches more wind than the back. */
.cells i svg g.b path{animation-duration:6.1s;animation-direction:reverse}
/* Per blade, within a tile. Three buckets covers the widest clump. */
.cells i svg path:nth-child(1){animation-delay:-.4s}
.cells i svg path:nth-child(2){animation-delay:-2.2s}
.cells i svg path:nth-child(3){animation-delay:-3.9s}
/* Per tile, across the field. Seven buckets rather than five, and seven
   because the grid is seven rows deep — five made every seventh column start
   together, which is a diagonal you can see once you have seen it. Seventy
   inline delays would say the same thing and be seventy things to maintain. */
.cells i:nth-child(7n+1) svg g{animation-delay:-.2s}
.cells i:nth-child(7n+2) svg g{animation-delay:-1.1s}
.cells i:nth-child(7n+3) svg g{animation-delay:-2.3s}
.cells i:nth-child(7n+4) svg g{animation-delay:-.7s}
.cells i:nth-child(7n+5) svg g{animation-delay:-1.8s}
.cells i:nth-child(7n+6) svg g{animation-delay:-2.9s}
.cells i:nth-child(7n+7) svg g{animation-delay:-1.4s}

/* ── the records tally ────────────────────────────────────────────────
   The number a candidate opened their history page to read, told apart from
   the sentence carrying it.

   Deliberately not naming that page in Korean here: this stylesheet is inlined
   into every page, so a UI string in a CSS comment is a UI string on the home
   page, and `an_expired_session_renders_the_page_signed_out` asserts the
   signed-out home page does not carry the words of the link it must not offer.
   A comment made that test fail once. Zero wrong earns the 정답 green; anything above zero
   gets the same coral a wrong choice is marked in on the question sheet, so
   the colour already means what it means before the number is read.

   On the number and not the row. A whole row washed in coral reads as an
   error state, and a paper you got three wrong on is not one — it is a paper
   with three questions left to do.

   A third state — no activity at all, kept muted — is the empty page rather
   than a row: the history view groups by paper and only lists papers that were
   actually attempted, so a row always counts at least one answer and a
   zero-of-zero row cannot be rendered. Somebody with nothing answered gets the
   `.msg` invitation instead, which is already muted.

   Note what is *not* written above: this stylesheet is inlined into every
   page, so any Korean UI phrase in a comment here is that phrase in the HTML
   of every page, and two different tests assert that a page does not contain
   copy it should not be offering. Both of them failed on a comment. Describe
   the copy; do not quote it. */
.tally{font-variant-numeric:tabular-nums;font-weight:var(--fw-semi)}
.tally.clean{color:var(--ok)}
.tally.todo{color:var(--bad)}

/* ── the result card ──────────────────────────────────────────────────
   Sized and weighted to be photographed. Everything a screenshot should not
   contain lives below it. */
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);
  padding:var(--space-5) var(--space-5);display:flex;flex-direction:column;gap:var(--space-4)}
.card-top{display:flex;justify-content:space-between;align-items:baseline;gap:var(--space-3)}
.card-brand{font-size:var(--text-meta);font-weight:var(--fw-semi);letter-spacing:-.01em;color:var(--accent)}
.card-date{font-size:var(--text-micro);color:var(--muted);font-variant-numeric:tabular-nums}
.score{display:flex;align-items:baseline;gap:var(--space-2)}
.score .big{font-size:var(--text-display);font-weight:var(--fw-bold);line-height:var(--lh-flat);letter-spacing:-.03em;
  color:var(--accent);font-variant-numeric:tabular-nums}
.score .of{font-size:var(--text-read);color:var(--muted);font-variant-numeric:tabular-nums}
.score .lab{margin-left:auto;font-size:var(--text-meta);color:var(--ink-2);text-align:right}
.card-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-3)}
.card-stat{background:var(--surface-2);border:1px solid var(--line-2);border-radius:var(--radius-lg);padding:var(--space-3) var(--space-3)}
.card-stat .k{font-size:var(--text-micro);letter-spacing:.05em;color:var(--muted);font-weight:var(--fw-semi)}
.card-stat .v{font-size:var(--text-ui);font-weight:var(--fw-semi);margin-top:var(--space-1);font-variant-numeric:tabular-nums}
.bars{display:flex;flex-direction:column;gap:var(--space-2)}
.bar{display:grid;grid-template-columns:86px 1fr 44px;gap:var(--space-3);align-items:center}
.bar .nm{min-width:0;display:flex;flex-direction:column;line-height:var(--lh-hero)}
.bar .nm .sub{font-size:var(--text-meta);color:var(--ink-2);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
/* Only printed when two rows share a subject name, so it is never noise: on
   the one card that needs it, it is the difference between one subject drawn
   from two different papers. */
.bar .qual{font-size:var(--text-micro);color:var(--muted);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
.bar .tr{height:6px;border-radius:var(--radius-pill);background:var(--line);overflow:hidden}
.bar .tr>span{display:block;height:100%;border-radius:var(--radius-pill);background:var(--accent)}
.bar .pc{font-size:var(--text-micro);color:var(--ink-2);text-align:right;font-variant-numeric:tabular-nums;
  font-weight:var(--fw-semi)}
/* The exam's own boundaries, not a gradient. 40% is 과락 and 60% is the pass
   line, so a candidate reads these four colours as the four things that can
   happen to a subject rather than as "more green is better". */
.bar .pc.cut{color:var(--bad)}
.bar .pc.low{color:var(--warn-ink)}
.bar .pc.pass{color:var(--ok)}
.bar .pc.all{color:var(--ok);font-weight:var(--fw-semi)}
/* `/me` lists subjects, where the result card lists papers, and the two
   columns
   either side of the track hold different things for it: a subject
   name can run to seven syllables where a paper's is clipped to a tag, and the
   figure carries the percentage as well as the tally, because this page is
   read for "where am I weak" rather than "what did I just score". The track,
   the four bands and the row rhythm are the component's own and are not
   touched — a 71% has to look the same on both pages. */
.bars.bysubject .bar{grid-template-columns:7.5rem 1fr 5.5rem}
.bars.bysubject .nm .sub{font-size:var(--text-ui);color:var(--ink)}
.bars.bysubject .pc .qual{margin-left:var(--space-2);color:var(--muted);font-weight:var(--fw-regular)}
/* A row is the way into drilling what it counts, so the whole row is the
   target — but only where there is something to drill, and the handler leaves
   the `href` off otherwise rather than offering a drill of nothing. */
.bars.bysubject a.bar{color:inherit;text-decoration:none}
/* The wrong-answer note. One line per question, because a notebook of eight
   is scanned and not read: the subject and number say which, the two marks say what went
   wrong, and the date says whether it still stings. The coral and the green
   are the same two the choices are graded in, so the glyphs already mean what
   they mean before the words beside them are read.

   Not `.wrong`, which is taken: `.choice.wrong` is a graded choice, and a bare
   `.wrong` rule would have matched every one of them and laid a radio out as a
   grid. */
.misses{display:flex;flex-direction:column}
.miss{display:grid;grid-template-columns:1fr auto auto;gap:var(--space-3);align-items:baseline;
  padding:var(--space-2) 0;border-top:1px solid var(--line-2);
  font-size:var(--text-meta);color:var(--ink-2);text-decoration:none}
.miss .qn{color:var(--ink);min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.miss .no{color:var(--bad);font-weight:var(--fw-semi)}
.miss .yes{color:var(--ok);font-weight:var(--fw-semi)}
.miss .ago{color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums}
.clock{font-size:var(--text-meta);font-weight:var(--fw-semi);font-variant-numeric:tabular-nums;color:var(--ink-2);
  padding:var(--space-1) var(--space-3);border-radius:var(--radius-pill);background:var(--surface-2);border:1px solid var(--line)}
.clock.low{color:var(--bad);background:var(--bad-soft);border-color:var(--bad-line)}
.simhint{font-size:var(--text-meta);color:var(--muted);text-align:center;margin:var(--space-3) 0 0;line-height:var(--lh-meta)}
/* One 직류 per card. The subject list is the thing a candidate recognises
   their own exam by, so it is on the card rather than behind a tap. */
.sits{display:grid;gap:var(--space-3);margin:var(--space-5) 0 0}
.sit{display:block;padding:var(--space-4) var(--space-4);border-radius:var(--radius-lg);text-decoration:none;
  background:var(--surface);border:1px solid var(--line);color:var(--ink)}
.sit-top{display:block;font-size:var(--text-micro);font-weight:var(--fw-semi);color:var(--muted);letter-spacing:.03em}
.sit-name{display:block;font-size:var(--text-read);font-weight:var(--fw-semi);margin:var(--space-1) 0 0}
.sit-meta{display:block;font-size:var(--text-meta);color:var(--ink-2);margin:var(--space-1) 0 0;font-variant-numeric:tabular-nums}
.sit-subs{display:block;font-size:var(--text-micro);color:var(--muted);margin:var(--space-2) 0 0;line-height:var(--lh-micro)}
/* The one filled surface above the bento, and the only place on the home
   screen that is not a tile. 오늘의 문제 is the accent inside the bento; this
   sits outside it, in the deep accent, because it is a different kind of thing
   rather than a more important tile. */
.simcta{display:flex;align-items:center;gap:var(--space-4);margin:var(--space-5) 0 0;padding:var(--space-4) var(--space-5);
  border-radius:var(--radius-lg);text-decoration:none;
  background:var(--accent-deep);color:var(--accent-ink);
  border:1px solid var(--accent-deep)}
.simcta-body{display:flex;flex-direction:column;gap:var(--space-1);min-width:0}
.simcta-k{font-size:var(--text-micro);font-weight:var(--fw-semi);letter-spacing:.06em;opacity:.75}
.simcta-v{font-size:var(--text-title);font-weight:var(--fw-bold);letter-spacing:-.01em}
.simcta-s{font-size:var(--text-meta);opacity:.8;font-variant-numeric:tabular-nums}
.simcta-go{margin-left:auto;font-size:var(--text-title);font-weight:var(--fw-bold);opacity:.8;flex:none}
/* 정답 변경됨. Amber rather than red: an appealed answer is worth noticing and
   is not an error — the paper was corrected, which is the system working. */
.changed{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-2) var(--space-3);margin:var(--space-3) 0 0;
  padding:var(--space-3) var(--space-3);border-radius:var(--radius-lg);font-size:var(--text-meta);font-weight:var(--fw-semi);
  background:var(--warn-soft);color:var(--warn-ink);border:1px solid var(--warn-line)}
.changed[hidden]{display:none}
.changed-why{font-weight:var(--fw-regular);opacity:.85}
/* The missed questions, handed back under the card. Closed by default and
   below the navigation: the card is what this page is for, and forty questions
   stacked above the two buttons would bury both. A <details> is one tap and no
   page load, which is the whole point — remediation has to happen while the
   mistake is still warm.

   Its summary's copy is not quoted here on purpose. This sheet ships inline in
   every page, and a test asserts that a candidate with a clean set is never
   shown that phrase. */
.wrongs{margin:var(--space-6) 0 0;border:1px solid var(--line);border-radius:var(--radius-lg);
  background:var(--surface);overflow:hidden}
.wrongs>summary{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-4) var(--space-5);font-weight:var(--fw-semi);cursor:pointer;list-style:none;color:var(--ink)}
.wrongs>summary::-webkit-details-marker{display:none}
/* The chevron is drawn here rather than left to the browser, because the
   default marker is hidden above to get the count onto the same line. */
.wrongs>summary::after{content:"";width:8px;height:8px;border-right:2px solid var(--muted);
  border-bottom:2px solid var(--muted);transform:rotate(45deg);margin-right:var(--space-2);
  transition:transform .18s ease}
.wrongs[open]>summary::after{transform:rotate(225deg)}
.wrongs>summary .n{margin-left:auto;font-size:var(--text-meta);font-weight:var(--fw-semi);color:var(--bad);
  font-variant-numeric:tabular-nums}
/* Full width and quiet: it sits directly under the summary, and a second
   primary-blue button there would compete with the page's own call to action
   just above the section for the same attention. */
.revealall{display:block;width:calc(100% - 32px);margin:0 var(--space-5) var(--space-4);padding:var(--space-3);
  font:inherit;font-weight:var(--fw-semi);font-size:var(--text-meta);color:var(--ink-2);cursor:pointer;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-md)}
.revealall[hidden]{display:none}
.wrongset{padding:var(--space-2) var(--space-5) var(--space-5);border-top:1px solid var(--line)}
.wrongset>h2{display:flex;align-items:baseline;gap:var(--space-3);font-size:var(--text-ui);margin:var(--space-5) 0 var(--space-1)}
.wrongset>h2 .n{font-size:var(--text-micro);font-weight:var(--fw-semi);color:var(--muted);
  font-variant-numeric:tabular-nums}
.wrongset>.meta{margin:0 0 var(--space-2)}
.wrongq{padding:var(--space-4) 0;border-top:1px solid var(--line)}
.wrongq:first-of-type{border-top:0}
.cardhint{font-size:var(--text-meta);color:var(--muted);margin:var(--space-4) 0 0;text-align:center}
/* The way back out of a single-question page, under its one control. */
.backto{font-size:var(--text-meta);margin:var(--space-5) 0 0}
/* Inside the card, because the card is what gets screenshotted and this is the
   sentence that has to travel with it. Tinted rather than loud: it sits under
   a real number and its job is to explain that number, not to outshout it. */
.cardpitch{margin:var(--space-3) 0 0;padding:var(--space-3) var(--space-3);border-radius:var(--radius-lg);
  background:var(--accent-soft);border:1px solid var(--accent-line);
  font-size:var(--text-meta);line-height:var(--lh-meta);color:var(--ink-2);text-align:center}
.cardpitch a{color:var(--accent);font-weight:var(--fw-semi);white-space:nowrap}
/* ── the conversion sheet ─────────────────────────────────────────────
   Over the set, not away from it: the runner keeps its questions in the DOM,
   so a navigation to the checkout throws the session away — and it throws it
   away for the person who was about to pay. */
.paywall{position:fixed;inset:0;z-index:40;display:flex;align-items:flex-end;justify-content:center}
.paywall[hidden]{display:none}
.paywall-scrim{position:absolute;inset:0;background:rgba(8,10,16,.45)}
.sheet{position:relative;width:100%;max-width:520px;background:var(--surface);
  border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  padding:var(--space-3) calc(var(--space-5) + env(safe-area-inset-right,0px))
          calc(var(--space-6) + env(safe-area-inset-bottom,0px))
          calc(var(--space-5) + env(safe-area-inset-left,0px));
  display:flex;flex-direction:column;gap:var(--space-3);max-height:90vh;overflow-y:auto;
  box-shadow:0 -14px 36px -20px rgba(8,10,16,.55)}
.handle{width:34px;height:4px;border-radius:var(--radius-pill);background:var(--line);align-self:center;margin:0 0 var(--space-1)}
.sheet h2{font-size:var(--text-read);font-weight:var(--fw-semi);letter-spacing:-.015em;margin:0;line-height:var(--lh-title)}
.sheet-sub{font-size:var(--text-meta);color:var(--muted);margin:0;line-height:var(--lh-meta)}
.valuebox{background:var(--surface-2);border:1px solid var(--line-2);border-radius:var(--radius-lg);
  padding:var(--space-3) var(--space-4);display:flex;flex-direction:column;gap:var(--space-2)}
.vrow{display:flex;align-items:baseline;gap:var(--space-3);font-size:var(--text-meta);line-height:var(--lh-meta)}
.vrow .ck{color:var(--accent);font-weight:var(--fw-semi);flex:0 0 auto;font-size:var(--text-micro)}
.vrow .tx{color:var(--ink-2);flex:1;min-width:0}
.vrow .tx b{color:var(--ink);font-weight:var(--fw-semi)}
.vrow.soft .ck{color:var(--muted)}
.vrow.soft .tx b{color:var(--ink-2)}
.pricerow{display:flex;align-items:baseline;gap:var(--space-2)}
.pricerow .p{font-size:var(--text-hero);font-weight:var(--fw-bold);letter-spacing:-.02em;color:var(--ink);
  font-variant-numeric:tabular-nums}
.pricerow .p .won{font-size:var(--text-ui);font-weight:var(--fw-semi)}
.pricerow .once{font-size:var(--text-meta);color:var(--muted);font-weight:var(--fw-medium)}
.cta{display:block;text-align:center;background:var(--accent);color:var(--accent-ink);
  border-radius:var(--radius-lg);padding:var(--space-4);font:inherit;font-size:var(--text-ui);font-weight:var(--fw-semi);
  letter-spacing:-.01em;text-decoration:none;border:0;cursor:pointer;width:100%}
.cta.ghost{background:transparent;color:var(--ink-2);border:1px solid var(--line);
  padding:var(--space-3);font-size:var(--text-meta)}
/* Required before the money, not after it. Small, and legible. */
.legal{font-size:var(--text-micro);line-height:var(--lh-ui);color:var(--muted);margin:var(--space-1) 0 0}
.trust{font-size:var(--text-micro);color:var(--muted);text-align:center;margin:0}

/* The runner hides its nav until the script pages; this one is always a pair
   of links and always shown. */
.runnernav.always{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin:var(--space-5) 0 0}
.runnernav.always .navbtn{display:block;text-align:center;text-decoration:none;line-height:var(--lh-title)}
.combo{font-size:var(--text-micro);font-weight:var(--fw-semi);padding:var(--space-1) var(--space-3);border-radius:var(--radius-pill);
  background:var(--ok-soft);color:var(--ok);border:1px solid var(--ok-line);
  flex:0 0 auto;font-variant-numeric:tabular-nums}
/* Seventy blades waving is exactly the kind of ambient motion this setting
   exists to stop, and it carries no information — the streak is in the colour
   and the density, both of which survive standing still. */
@media (prefers-reduced-motion:reduce){
  .rail>span{transition:none}
  .choice{transition:none}
  .dangerbtn{transition:none}
  .cells i svg g,.cells i svg path{animation:none}
  /* Stopped at the start of the cycle, where the visible third of the gradient
     is plain ink — so the word simply reads as the wordmark, rather than
     freezing mid-sweep with one half of it blue. */
  .pocket span{animation:none;transform:none;color:var(--ink)}
}
header h1{font-size:var(--text-title);margin:0;letter-spacing:-.01em}
.meta{font-size:var(--text-meta);color:var(--muted)}
.q{border-top:1px solid var(--line-2);padding:var(--space-7) 0}
.q:first-of-type{border-top:0;padding-top:var(--space-3)}
.qno{font-weight:var(--fw-semi);margin:0 0 var(--space-3);color:var(--accent);font-size:var(--text-meta);letter-spacing:.02em}
.qno .pts{font-weight:var(--fw-regular);font-size:var(--text-meta);color:var(--muted);margin-left:var(--space-2)}
/* The border below is the one part of a choice's box that the question sheet
   cannot know about, and a matching question's header is indented past it. Not
   an override of layout — it tells the sheet's own calc what this shell adds. */
:root{
  /* What this shell adds to a choice's box, so the question sheet's own calc
     can account for it. Not an override of layout — a declaration of it. */
  --choice-bd:1px;
  /* The shell draws the border that parts one choice from the next, so the
     mark goes back to being a quiet numeral. The sheet's own value is the
     accent, which is what the borderless dump preview needs. */
  --choice-mark-ink:var(--muted);
  /* Two shades only the shell needs: a pressed accent and a heading accent
     that still reads against a tinted explanation panel. */
  --accent-hover:#1E4FCC; --accent-deep:#12325E; --warn-ink:#6B5514;
  /* The 잔디 scale: empty, then four steps toward the accent. Chrome-only,
     like the shades above — the question sheet never draws a streak. */
  --g0:#E7EAF3; --g1:#C9D9FF; --g2:#9DBBFF; --g3:#6494FF; --g4:#2563EB;
  /* One day's tile. It has been wrong in both directions — `1fr` across ten
     columns made it 36px and the widget a third of a phone screen; a flat 16px
     overcorrected into something you squint at — and then wrong again by being
     too timid about it: 18px on every phone and 26px on every desktop is
     legible and forgettable, and a streak is meant to be a thing you are proud
     of rather than a thing you can make out.
     24px floor, 5vw so it grows with the viewport, 36px ceiling. Ten weeks at
     the ceiling is 396px against a 650px card, so it is still a widget; at the
     floor it is 276px, which fits every phone from 360px up. */
  --cell:clamp(24px, 5vw, 36px);
  /* Space between days. It grows with the cell or the field turns into a
     single block of colour at 36px. */
  --cellgap:4px;
  /* What the sticky filter bar sits under, and a fallback rather than the
     answer: the script measures the nav and writes the real value here on load
     and on resize.
     Measured rather than derived because deriving it was wrong. 42px of tap
     target plus 9px of padding each side plus the 1px border is 61, which is
     what this said — and the nav renders 67. The filter bar sat six pixels
     under the nav while scrolling, which is precisely the drift a token was
     supposed to prevent; a hand-computed constant is a second source of truth
     wearing a token's clothes. 67 keeps it right with JavaScript off. */
  --navh:67px;
  /* 잔디 is green, and this palette is blue. Four steps of its own rather than
     reusing --g1..--g4, because grass drawn in the accent colour is not grass
     — it is the same squares wearing a different shape. --soil is what a day
     with nothing on it looks like: ground, not absence. */
  --soil:#E9ECF2; --grass-1:#A8D5A2; --grass-2:#6EBE67; --grass-3:#3D9B41; --grass-4:#227A2E;
  --bloom:#FFFFFF; --bloom-eye:#F2B705;
}
/* Below about 346px the 24px floor stops fitting, and the arithmetic is worth
   writing down because it is not obvious: `main` is `content-box` with 20px of
   padding each side, and the card takes another 14px plus a 1px border, so a
   320px phone leaves the field 250px. Ten 24px days and nine 4px gaps is 276.
   The field would hang 26px out of its own card and give the whole page a
   horizontal scrollbar.

   Measured at 320, 346 and 360 rather than derived: the first attempt at this
   sum was wrong by 40px, because `content-box` means a width set on `main` is
   the width *inside* its padding. 346 is where 24px starts to fit, and 359 is
   the next round number below the 360px phones that are the real floor now. */
@media (max-width:359px){
  :root{--cell:20px;--cellgap:3px}
  /* The purchase sheet gives up the same room the column does. */
  .sheet{padding-left:calc(var(--space-4) + env(safe-area-inset-left,0px));
         padding-right:calc(var(--space-4) + env(safe-area-inset-right,0px))}
  /* The wordmark will not wrap, so on the narrowest screens the buttons give
     up the room instead: 50px each is 150px of tap target the 109px name
     cannot fit beside inside 292px of nav. 44px still clears the 44px
     guideline and buys back eighteen. */
  .iconbtn{min-width:44px;padding:var(--space-1) var(--space-1)}

}
@media (prefers-color-scheme:dark){
  :root{
    --accent-hover:#7BA3FF; --accent-deep:#C7D8FF; --warn-ink:#E8CE93;
    --g0:#1C2230; --g1:#23386B; --g2:#2F52A8; --g3:#3F6FDA; --g4:#5C8BFF;
    /* Darker and less saturated: full-brightness green on a near-black panel
       glares, and seventy of them glare seventy times. */
    --soil:#1B2130; --grass-1:#2F5638; --grass-2:#3E7A46; --grass-3:#4FA255; --grass-4:#6BC46B;
    --bloom:#F4F8FB; --bloom-eye:#E8B02F;
  }
}
/* The choice's own row. The question sheet makes `.choice` a flex container
   and WebKit ignores that on a `<button>`, laying the content out in an
   anonymous block instead — so the mark stacked above its chart rather than
   sitting beside it. This is an ordinary element and every engine treats it
   the same; the button keeps the padding and the frame.

   `align-items:flex-start` because a choice may be a chart several hundred
   pixels tall, and the mark belongs at the top of it rather than halfway down
   the side. */
.crow{display:flex;gap:var(--choice-gap);align-items:flex-start;width:100%;min-width:0}
/* A choice whose body opens with a table starts its first line one cell's
   padding down, inside the cell — so the mark beside it has to clear the same
   distance or it sits above the title it names rather than beside it. Measured
   across the corpus: 18px out, which is the wrapper's own top margin (dropped
   above) plus this. `align-items:baseline` does not help — a flex item whose
   first child is a table synthesises a baseline of its own and lands 30px out. */
.crow:has(> .cbody > .tablewrap:first-child) > .mark{padding-top:var(--cell-pad-y)}
.choice{width:100%;text-align:left;background:var(--surface);border:1px solid var(--line);
  font:inherit;color:var(--ink);cursor:pointer;
  transition:background-color .13s ease,border-color .13s ease,transform .13s ease}
/* Hover sits here and not in the gathered block at the foot of the sheet,
   because `.choice:hover` and `.choice[aria-checked="true"]` have the same
   specificity and the later rule would win: hovering an answer you had already
   chosen would paint it unchosen. Declared above the state rules, it yields to
   them, which is right — a choice that is picked, or graded, has nothing more
   to offer a pointer. */
@media (hover:hover){
  .choice:hover{background:var(--surface-2);border-color:var(--line-strong)}
}
/* Not behind `hover:hover`. A touch device fires `:active` on tap and that is
   exactly where the press wants acknowledging — it is the feedback the
   `<button>` gave for free until this became a `<div role="radio">`. */
.choice:active{transform:translateY(1px)}
.choice[aria-checked="true"]{background:var(--accent-soft);border-color:var(--accent-line)}
.choice.correct{background:var(--ok-soft);border-color:var(--ok)}
.choice.wrong{background:var(--bad-soft);border-color:var(--bad)}
.reveal,.go{display:block;width:100%;padding:var(--space-4);margin:var(--space-7) 0 0;font:inherit;font-weight:var(--fw-semi);
  color:var(--accent-ink);background:var(--accent);border:0;border-radius:var(--radius-md);cursor:pointer}
/* The script hides the reveal control once it has fired, but `display:block`
   above is an author rule and the `hidden` attribute takes effect through the
   UA sheet's `[hidden]{display:none}` — author beats UA regardless of
   specificity, so without this the attribute is set and nothing happens,
   leaving a "정답 보기" button above answers already shown. */
.reveal[hidden],.go[hidden]{display:none}
.note{font-size:var(--text-meta);color:var(--ink-2);background:var(--surface-2);border-radius:var(--radius);padding:var(--space-3) var(--space-4);margin:var(--space-4) 0 0;line-height:var(--lh-read)}
.explain{margin:var(--space-4) 0 0;padding:var(--space-5) var(--space-5);background:var(--surface-3);border:1px solid var(--line-2);border-left:3px solid var(--accent);border-radius:0 var(--radius-md) var(--radius-md) 0;line-height:var(--lh-read)}
.explain h2{font-size:var(--text-ui);margin:var(--space-5) 0 var(--space-2);color:var(--accent-deep)}
.explain h2:first-child{margin-top:0}
.explain p{margin:var(--space-3) 0}
.explain strong{color:var(--accent-deep)}
.explain ul,.explain ol{margin:var(--space-3) 0;padding-left:var(--space-6)}
.explain table{border-collapse:collapse;margin:var(--space-3) 0}
.explain td,.explain th{border:1px solid var(--line-strong);padding:var(--space-2) var(--space-3)}
/* Not hidden: this one is the call to action and belongs before the reveal.
   It is the only place the product is offered at the moment somebody wants it,
   so it is weighted to be noticed rather than to blend in. */
.locked{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;font-size:var(--text-meta);color:var(--warn-ink);
  background:var(--warn-soft);border:1px solid var(--warn-line);border-radius:var(--radius-md);padding:var(--space-4) var(--space-4);margin:var(--space-4) 0 0}
.locked a{font-weight:var(--fw-semi);color:var(--accent);text-decoration:none;border-bottom:1px solid currentColor;padding-bottom:var(--space-hair)}
form.pick{margin:0 0 var(--space-5)}
form.pick label{display:inline-block;margin:0 var(--space-4) var(--space-3) 0}
/* `font:inherit` is a shorthand and resets size with it, so the 16px floor
   above loses to it on specificity. Restated after. */
input[type=search]{width:100%;padding:var(--space-3);font:inherit;font-size:16px;
  border:1px solid var(--line-strong);border-radius:var(--radius-md)}
/* Jumping to `#q14` puts the question's top edge at viewport 0 — which is
   underneath the sticky nav, so the first thing a search hit hides is the
   question number it just promised. `scroll-margin-top` is the anchor's own
   offset and costs nothing when nobody is jumping anywhere; `--navh` is the
   measured nav height, so the two cannot drift. */
section.q{scroll-margin-top:calc(var(--navh) + var(--space-3))}
.hit{border-top:1px solid var(--line-2);padding:var(--space-4) 0}
.hit .snip{margin:0 0 var(--space-2)}
/* A hit's links are the tap targets, and on a phone they were inline text
   separated by a space — two 12px words a thumb has to land between. Wrapped
   pills with their own padding instead, so each placement is a target rather
   than a piece of a sentence. */
.searchform{margin:0 0 var(--space-5)}
.hitlink{display:inline-flex;align-items:center;min-height:44px;margin:0 var(--space-2) var(--space-2) 0;
  padding:0 var(--space-3);border:1px solid var(--line);border-radius:var(--radius-pill);
  font-size:var(--text-meta);font-weight:var(--fw-semi);color:var(--accent);text-decoration:none;
  white-space:nowrap;-webkit-tap-highlight-color:transparent}
form.inline{display:inline;margin:0}
.linkish{background:none;border:0;padding:0;font:inherit;color:var(--accent);cursor:pointer}

/* ── the header ───────────────────────────────────────────────────────
   Was four text links separated by interpuncts, which read as a wireframe
   because it had no rule at all — this is the first CSS the nav has ever had.

   Sticky, because on a 25-question paper the way out was a scroll to the top.
   The brand doubles as the home link: a wordmark is a bigger tap target than
   an arrow and says where "home" is, which an arrow does not. */
nav{position:sticky;top:0;z-index:20;display:flex;align-items:center;
  justify-content:space-between;gap:var(--space-3);padding:var(--space-3) var(--space-4);
  /* Out past the column's padding to within 4px of the screen, so the
     rule under it reads as the top of the page rather than the top of
     the text. In terms of `--gutter`, never a number: it was `-16px`,
     which is right only while the padding is 20. */
  margin:0 calc(4px - var(--gutter)) var(--space-6);
  background:var(--surface);border-bottom:1px solid var(--line)}
/* `nowrap`, and it is not tidiness. The two-word wordmark is 109px at 14px
   bold against the one-word version's 62, and on a 320px screen the extra
   word wrapped to a second line: the wordmark doubled in height and took the
   nav from 67px to 68 and climbing. This is the second time a two-word label
   has done that to this header — the sign-in button's full phrase did it
   first, taking the nav from 60px to 95px — so the rule is on the element
   rather than left to the length of whatever the text becomes next.
   `flex:0 0 auto` so it is the icon buttons that give up room, not the name. */
.brand{display:inline-flex;align-items:center;gap:var(--space-2);font-size:var(--text-title);font-weight:var(--fw-semi);
  color:var(--ink);text-decoration:none;letter-spacing:-.01em;
  white-space:nowrap;flex:0 0 auto}
.brand svg{color:var(--accent)}
/* The wordmark leads now, so the mark beside it is sized to sit with it rather
   than with the 20px action icons on the other side of the nav. */
.brand svg.i{width:28px;height:28px}
/* The wordmark gives the room back on the narrowest screens. Its own block
   rather than the `max-width:359px` one further up, and that is not tidiness:
   that block is declared *before* `.brand`, so a rule placed in it loses to
   the base rule on source order and the step-down silently never happens.
   Measured in the rendered nav: at `--text-title` with a 28px mark the brand
   sets 151.5px, and beside 154px of actions that is 305.5px against the 280px
   a 320px screen has — 25px over, which is the overflow this breakpoint has
   existed to prevent since the rename. At `--text-ui` with a 22px mark it is
   111.4px and the row comes to 265.4px. */
@media (max-width:359px){
  .brand{font-size:var(--text-ui)}
  .brand svg.i{width:22px;height:22px}
}
.pocket{display:inline-block}
/* Each letter its own element, lifted in turn.
   This replaces a sheen that swept the word every six seconds. That sheen was
   chosen over a bounce for two stated reasons, and both are answered rather
   than ignored. It moved no geometry: a `transform` moves none either — it is
   composited, so a lifted letter reflows nothing and cannot widen the nav. And
   it was still for most of its cycle because a header is on every page: this
   wave travels for about a fifth of a 5s loop and rests for the other four,
   and the lift is .14em — a nudge, not a bounce.
   `display:inline-block` is what makes a letter transformable at all, and is
   the one real cost: inline-block glyphs do not kern against their
   neighbours, so the word sets a hair wider than it did. Measured below in the
   nav's own width, not assumed.
   The colour goes accent, then green, then back. Not `--accent-ink`: that
   token is the text colour *on* an accent fill — white in the light theme —
   so a letter wearing it would simply vanish against the page. */
/* `--i` is the letter's index and arrives inline, per span. Declared here as
   well so it is a token like any other — the guard that checks every `var()`
   resolves at the base level is right to ask, and a letter that somehow
   arrived without one should start the wave rather than break the `calc`. */
.pocket span{--i:0;display:inline-block;animation:bob 5s ease-in-out infinite;
  animation-delay:calc(var(--i) * 80ms)}
@keyframes bob{
  0%,78%,100%{transform:translateY(0);color:var(--ink)}
  6%{transform:translateY(-.14em);color:var(--accent)}
  14%{transform:translateY(0);color:var(--ok)}
  22%{transform:translateY(0);color:var(--ink)}
}
.navacts{display:flex;align-items:center;gap:var(--space-1)}
/* One shape for a link and a submit button, so the logout form does not have
   to look like the odd one out. 44px is the tap target; the icon is 20. */
/* Shown only in an installed window, where there is no browser back button
   to duplicate. `display-mode:standalone` is the query that asks exactly
   that. `[hidden]` still wins: the script hides it on the first page of a
   session, where there is nothing behind it to go back to. */
.navback{display:none;flex:0 0 auto;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;margin:0 var(--space-1) 0 calc(var(--space-2) * -1);
  padding:0;border:0;border-radius:var(--radius-lg);background:none;color:var(--ink-2);cursor:pointer}
@media (display-mode:standalone){.navback{display:inline-flex}
  /* Already installed. Offering to install it again is the clearest way to
     say the product does not know what it is. */
  .install{display:none}}
/* Both set `display`, and both are written with `hidden`, so both have to
   say what `hidden` means to them or the attribute does nothing. */
.iconbtn[hidden],.install[hidden]{display:none}
.navback[hidden]{display:none}
.iconbtn{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--space-1);min-width:50px;min-height:44px;padding:var(--space-1) var(--space-2);border:0;border-radius:var(--radius-lg);
  background:none;font:inherit;color:var(--ink-2);text-decoration:none;cursor:pointer;
  -webkit-tap-highlight-color:transparent}
.iconbtn .lbl{font-size:var(--text-micro);font-weight:var(--fw-semi);letter-spacing:-.01em;line-height:var(--lh-flat);
  white-space:nowrap}
/* The one action worth colouring: it is what the guest funnel is asking for.
   NOT `.go` — that name is already the full-width call-to-action above, and
   composing with it inherited `width:100%` and `margin:var(--space-7) 0 0`, which
   dropped this button var(--space-7) below its siblings inside the header. Class names
   are a shared namespace; a modifier has to pick one nothing else owns. */
.iconbtn.signin{color:var(--accent)}
svg.i{width:20px;height:20px;display:block;fill:none;stroke:currentColor;
  stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.msg{background:var(--warn-soft);border-left:3px solid var(--warn);padding:var(--space-3) var(--space-4);border-radius:0 var(--radius-sm) var(--radius-sm) 0}
/* ── the deletion zone ───────────────────────────────────────────────
   The one irreversible control in the product, kept away from the page's own
   actions. It used to be `.go`: full width, accent filled, indistinguishable
   from 정답 보기 and 시작, in the content flow directly under the last row of
   a candidate's record. A rule above it and an outline in the wrong-answer
   coral say what it does without competing for the eye — and it is the one
   button on the site that should be harder to reach, not easier. */
.danger{margin:var(--space-8) 0 0;padding:var(--space-5) 0 0;border-top:1px solid var(--line-2)}
.danger h2{font-size:var(--text-meta);font-weight:var(--fw-semi);color:var(--muted);
  letter-spacing:.06em;margin:0 0 var(--space-3)}
/* The consent had no rule at all — a bare checkbox with its sentence wrapping
   underneath it rather than beside it, which is how a line of legal copy ends
   up looking like body text nobody reads. */
.confirm{display:flex;gap:var(--space-2);align-items:flex-start;margin:0 0 var(--space-4);
  font-size:var(--text-meta);color:var(--ink-2);line-height:var(--lh-meta);cursor:pointer}
.confirm input{flex:none;margin:var(--space-1) 0 0;accent-color:var(--bad)}
.dangerbtn{font:inherit;font-size:var(--text-meta);font-weight:var(--fw-medium);color:var(--bad);
  background:none;border:1px solid var(--bad-line);border-radius:var(--radius-md);
  padding:var(--space-2) var(--space-4);cursor:pointer;
  transition:background-color .13s ease,border-color .13s ease}
/* Every hover the app has, in one place and behind a pointer that can
   actually hover. On a touch screen `:hover` sticks after a tap until
   something else is tapped, so a tile stayed lit long after the finger left.
   Order within the block is the order they were written in, and each one is
   `X:hover` against a base `X`, so every rule still outranks what it modifies
   no matter where the block sits. `.choice:hover` is deliberately not here —
   see the note at `.choice`. */
@media (hover:hover){
  .tile:hover{border-color:var(--accent-line)}
  .tile.hero:hover{border-color:var(--accent)}
  .papers a:hover{background:var(--surface-2)}
  .fchip:hover{background:var(--surface-2);color:var(--ink)}
  .fchip[aria-pressed="true"]:hover{background:var(--accent-hover)}
  .xbtn:hover{color:var(--ink)}
  .sit:hover{border-color:var(--accent)}
  .simcta:hover{background:var(--accent)}
  .revealall:hover{border-color:var(--accent);color:var(--accent)}
  .reveal:hover,.go:hover{background:var(--accent-hover)}
  .locked a:hover{color:var(--accent-hover)}
  .hitlink:hover{background:var(--accent-soft);border-color:var(--accent-line)}
  .iconbtn:hover{background:var(--surface-2);color:var(--ink)}
  .iconbtn.signin:hover{background:var(--accent-soft)}
  .dangerbtn:hover{background:var(--bad-soft);border-color:var(--bad)}
  .bars.bysubject a.bar[href]:hover .sub{color:var(--accent)}
  .miss:hover .qn{color:var(--accent)}
}
