/* Reset & Base */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Breakpoints. CSS cannot use custom properties inside a media query, so the
   two the app uses are written out literally everywhere and recorded here:

     40rem (640px)  narrow → roomy: phone portrait leaves, tablet portrait enters
     60rem (960px)  roomy → wide:   the container stops growing

   Everything is mobile-first: the unqualified rules are the narrow case and
   min-width queries add back what a larger screen can afford. */

:root {
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-danger: #dc2626;
  --color-danger-hover: #b91c1c;
  --color-success: #16a34a;
  --color-warning: #d97706;
  --color-info: #0891b2;
  --color-muted: #6b7280;
  --color-bg: #ffffff;
  --color-bg-alt: #f9fafb;
  /* A raised face — the card, a button. The same white as the page here, which
     is why light mode never needed the distinction; on a dark ground the page
     is the darkest thing and a panel has to sit above it. */
  --color-surface: #ffffff;
  /* Text on a solid accent. A literal #fff in three places until now, and a
     literal cannot be flipped: on a dark page the accent is a lighter blue and
     wants dark text on it (5.6 against white's 3.5). */
  --color-on-accent: #ffffff;
  --color-border: #e5e7eb;
  --color-text: #111827;
  --color-text-secondary: #6b7280;
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", monospace;

  /* One border colour for structure (the hairlines that divide a page) and a
     stronger one for anything you can press or type into. Interactive edges
     have to be findable; dividing lines have to stay out of the way. */
  --color-border-strong: #d1d5db;

  /* Softer than the browser's own square-ish chrome — the cheapest single
     change that stops the app reading as an unstyled form. --radius-lg is for
     panels, which need a visibly larger curve than the controls inside them. */
  --radius: 6px;
  --radius-lg: 10px;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

  /* The four ratings are one palette, used by both the study buttons and the
     history badges. They were written out twice before, which is how two
     surfaces drift into disagreeing about what "hard" looks like. */
  --rating-again-bg: #fef2f2;
  --rating-again-fg: #991b1b;
  --rating-again-edge: #fca5a5;
  --rating-hard-bg: #fff7ed;
  --rating-hard-fg: #9a3412;
  --rating-hard-edge: #fdba74;
  --rating-good-bg: #f0fdf4;
  --rating-good-fg: #166534;
  --rating-good-edge: #86efac;
  --rating-easy-bg: #eff6ff;
  --rating-easy-fg: #1e40af;
  --rating-easy-edge: #93c5fd;

  /* Spacing scale. Use these rather than ad-hoc rem values so that vertical
     rhythm stays consistent when a component is restyled. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* Type scale. Fixed sizes are for UI chrome; body copy and headings below
     are fluid. */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  /* Above body copy the scale is for chrome that is deliberately large — the
     brand, a card's question, a headline figure — rather than for headings,
     which stay fluid. */
  --text-lg: 1.25rem;
  --text-xl: 1.5rem;
  --text-2xl: 2rem;

  /* Small capitalised labels: table headings and the like. Uppercase without
     extra tracking sets too tight to read at this size. */
  --tracking-label: 0.05em;

  /* Minimum comfortable touch area (44px). Applied where a pointer is coarse,
     not unconditionally — a mouse does not need it and it wastes space. */
  --tap-target: 2.75rem;

  --container-max: 60rem;
  --container-pad: var(--space-4);

  /* Measure: the comfortable line length for running prose. The container is
     sized for tables and forms, which is far too wide to read a paragraph
     across. */
  --measure: 38rem;
}

html {
  font-family: var(--font-sans);
  /* 100% rather than a fixed 16px so a reader who has raised their browser's
     default font size keeps it. Every rem below scales with them. */
  font-size: 100%;
  line-height: 1.6;
  /* Stop a phone from inflating body text in landscape. */
  -webkit-text-size-adjust: 100%;
}

body {
  /* On body, not html: the dark palette is keyed on body[data-theme], and
     html cannot see variables redefined on its child. With html unpainted the
     browser takes body's background for the whole canvas. */
  color: var(--color-text);
  background: var(--color-bg);
  min-height: 100vh;
  /* A long unbroken string — a token, a URL, a pasted identifier — must wrap
     rather than push the whole page sideways. */
  overflow-wrap: break-word;
}

/* The hidden attribute must beat any author display rule. The browser's own
   `[hidden] { display: none }` lives in the user-agent origin, so a component
   that sets its own display — a flex row, a grid — stays visible while marked
   hidden. That is how the study page shipped its rating buttons on screen
   before the answer was revealed. Stimulus toggles .hidden all over this app,
   so the guard belongs here rather than on each component. */
[hidden] {
  display: none !important;
}

/* Media never overflows its column. */
img, video, canvas, svg {
  max-width: 100%;
  height: auto;
}

h1, h2, h3, h4, h5, h6 {
  line-height: 1.3;
  margin-bottom: 0.5em;
  /* Headings are the one place a long word is likely to be unbreakable and
     visually damaging, so allow a hard break as a last resort. */
  overflow-wrap: break-word;
}

/* Fluid headings: the first value is the narrow floor, the last the wide
   ceiling, and the middle term interpolates between them. */
h1 { font-size: clamp(1.5rem, 1.28rem + 1.1vw, 1.75rem); }
h2 { font-size: clamp(1.25rem, 1.14rem + 0.55vw, 1.375rem); }
h3 { font-size: clamp(1.0625rem, 1.0rem + 0.3vw, 1.125rem); }

p { margin-bottom: 0.75em; }

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

ul, ol {
  padding-left: 1.5em;
  margin-bottom: 0.75em;
}

/* Metadata blocks on show pages. As a bare dl these read as loose text with
   some bold in it; as a panelled two-column grid they read as a record. The
   single column below 40rem is the same grid with the value under its label. */
dl {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-1) var(--space-5);
  margin-bottom: var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  /* White, like .table-wrap: both are a panel of data, and they should agree.
     The tinted panel is reserved for the stat cards, which are an accent. */
  background: var(--color-bg);
}

dt {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-text-secondary);
}

dd {
  margin-left: 0;
  margin-bottom: var(--space-3);
  color: var(--color-text);
}

dd:last-child {
  margin-bottom: 0;
}

section {
  margin-bottom: var(--space-7);
}

section > h2 {
  margin-bottom: var(--space-4);
}

.text-muted {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.text-danger {
  color: var(--color-danger);
}

/* Roomy and up: the container can afford more breathing room, and a metadata
   record can put its labels in a column beside the values. */
@media (min-width: 40rem) {
  :root {
    --container-pad: var(--space-5);
  }

  dl {
    grid-template-columns: minmax(7rem, max-content) 1fr;
    padding: var(--space-5);
  }

  dt {
    /* Cap-height of the label against x-height of the value: nudge it down so
       the two sit on the same visual line. */
    padding-top: 0.15em;
  }

  dd {
    margin-bottom: var(--space-2);
  }
}

/* The dark palette — every page on an Android phone (web_android.md §4.1.1).
   Designed for the study terminal, widened to the whole phone by the owner on
   15 September 2026. The desktop keeps the light one.

   Designed by preserving the light palette's *relationships*, not its values,
   and checked with numbers rather than by eye. Contrast against the page,
   light then dark: text 17.7 / 15.3, secondary 4.8 / 5.0, primary 5.2 / 5.3,
   label-on-accent 5.2 / 5.6, structural hairline 1.24 / 1.42. The three that
   read higher in dark — success 3.3 / 6.0, warning 3.2 / 7.2, info 3.7 / 7.6 —
   are not drift: those light values are below 4.5 and matching a deficiency
   would be the wrong reading of "preserve the relationships".

   ⚠️ The block appears twice because CSS cannot share one declaration block
   across a media query, and the two cases are genuinely different: an explicit
   "dark" always applies, while "system" defers to the device. Keep them in
   step — they are adjacent for that reason.

   ⚠️ What numbers cannot settle: whether the four ratings are tellable apart at
   a glance without reading the label. That is hue perception on a particular
   screen at a particular brightness. If they are not, raise the chroma of the
   labels and edges — do not darken the faces further. */

body[data-theme="dark"] {
  /* Native controls — inputs, selects, scrollbars — follow the page. */
  color-scheme: dark;

  --color-bg: #11151b;
  --color-bg-alt: #1a1f28;
  --color-surface: #1e242e;
  --color-border: #2b323d;
  --color-border-strong: #3d4653;
  --color-text: #e8ebef;
  --color-text-secondary: #7d8695;
  --color-muted: #7d8695;
  --color-primary: #4a8ae8;
  --color-primary-hover: #5d9af5;
  --color-on-accent: #0b0f14;
  --color-danger: #f0676c;
  --color-danger-hover: #ff8288;
  --color-success: #36a85f;
  --color-warning: #e0913a;
  --color-info: #35b6cf;

  /* Elevation is carried by the surface being lighter than the page; a black
     shadow on a dark ground is invisible. This one only softens the card's
     edge. */
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

  /* The ratings invert which channel carries the distinction. In light the
     four pale faces do it, because a light tint can hold hue. At low lightness
     chroma collapses, which is why darkening the light tints naively lands all
     four on near-black — so here the face is a bare suggestion and the label
     and edge, which are light, carry the colour. Hue separation of the four
     labels: the closest pair is again/hard at 32 degrees, against 15 in the
     light palette, so they are further apart here, not closer. */
  --rating-again-bg: #2b1618;
  --rating-again-fg: #ff9a9a;
  --rating-again-edge: #7f3a3f;
  --rating-hard-bg: #2d1f12;
  --rating-hard-fg: #ffc078;
  --rating-hard-edge: #8a5a2b;
  --rating-good-bg: #14271a;
  --rating-good-fg: #86e0a3;
  --rating-good-edge: #356b45;
  --rating-easy-bg: #15203a;
  --rating-easy-fg: #9dc0ff;
  --rating-easy-edge: #3a5590;

  /* The learning-state badges of the note lists, in the ratings' pattern:
     a bare face, the colour in label and edge. components.css falls back to
     the light literals, so only these blocks define them. */
  --badge-new-bg: #15203a;
  --badge-new-fg: #9dc0ff;
  --badge-new-edge: #3a5590;
  --badge-learning-bg: #2d1f12;
  --badge-learning-fg: #ffc078;
  --badge-learning-edge: #8a5a2b;
  --badge-review-bg: #14271a;
  --badge-review-fg: #86e0a3;
  --badge-review-edge: #356b45;
  --badge-suspended-bg: #1a1f28;
  --badge-suspended-fg: #9aa3b0;
  --badge-suspended-edge: #3d4653;
  --badge-buried-bg: #1a1f28;
  --badge-buried-fg: #7d8695;
  --badge-buried-edge: #2b323d;
  --badge-cards-marked-bg: #231a3a;
  --badge-cards-marked-fg: #c9b3ff;
  --badge-cards-marked-edge: #5b4690;
}

@media (prefers-color-scheme: dark) {
  body[data-theme="system"] {
    /* Native controls — inputs, selects, scrollbars — follow the page. */
    color-scheme: dark;

    --color-bg: #11151b;
    --color-bg-alt: #1a1f28;
    --color-surface: #1e242e;
    --color-border: #2b323d;
    --color-border-strong: #3d4653;
    --color-text: #e8ebef;
    --color-text-secondary: #7d8695;
    --color-muted: #7d8695;
    --color-primary: #4a8ae8;
    --color-primary-hover: #5d9af5;
    --color-on-accent: #0b0f14;
    --color-danger: #f0676c;
    --color-danger-hover: #ff8288;
    --color-success: #36a85f;
    --color-warning: #e0913a;
    --color-info: #35b6cf;

    /* Elevation is carried by the surface being lighter than the page; a black
       shadow on a dark ground is invisible. This one only softens the card's
       edge. */
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.5);

    /* The ratings invert which channel carries the distinction. In light the
       four pale faces do it, because a light tint can hold hue. At low lightness
       chroma collapses, which is why darkening the light tints naively lands all
       four on near-black — so here the face is a bare suggestion and the label
       and edge, which are light, carry the colour. Hue separation of the four
       labels: the closest pair is again/hard at 32 degrees, against 15 in the
       light palette, so they are further apart here, not closer. */
    --rating-again-bg: #2b1618;
    --rating-again-fg: #ff9a9a;
    --rating-again-edge: #7f3a3f;
    --rating-hard-bg: #2d1f12;
    --rating-hard-fg: #ffc078;
    --rating-hard-edge: #8a5a2b;
    --rating-good-bg: #14271a;
    --rating-good-fg: #86e0a3;
    --rating-good-edge: #356b45;
    --rating-easy-bg: #15203a;
    --rating-easy-fg: #9dc0ff;
    --rating-easy-edge: #3a5590;

    /* The learning-state badges of the note lists, in the ratings' pattern:
       a bare face, the colour in label and edge. components.css falls back to
       the light literals, so only these blocks define them. */
    --badge-new-bg: #15203a;
    --badge-new-fg: #9dc0ff;
    --badge-new-edge: #3a5590;
    --badge-learning-bg: #2d1f12;
    --badge-learning-fg: #ffc078;
    --badge-learning-edge: #8a5a2b;
    --badge-review-bg: #14271a;
    --badge-review-fg: #86e0a3;
    --badge-review-edge: #356b45;
    --badge-suspended-bg: #1a1f28;
    --badge-suspended-fg: #9aa3b0;
    --badge-suspended-edge: #3d4653;
    --badge-buried-bg: #1a1f28;
    --badge-buried-fg: #7d8695;
    --badge-buried-edge: #2b323d;
    --badge-cards-marked-bg: #231a3a;
    --badge-cards-marked-fg: #c9b3ff;
    --badge-cards-marked-edge: #5b4690;
  }
}
/* Buttons */
.btn {
  display: inline-block;
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  /* 600, not 500: a button has to read as a thing you press even when it is
     the plain variant sitting next to body text. */
  font-weight: 600;
  line-height: 1.4;
  /* The interactive border, not the structural one — a control whose edge is
     the same weight as a page divider does not look pressable. */
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
  text-decoration: none;
  vertical-align: middle;
}

.btn:hover {
  background: var(--color-bg-alt);
  text-decoration: none;
}

.btn-primary {
  background: var(--color-primary);
  color: var(--color-on-accent);
  border-color: var(--color-primary);
}

.btn-primary:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
}

.btn-danger {
  background: var(--color-danger);
  color: var(--color-on-accent);
  border-color: var(--color-danger);
}

.btn-danger:hover {
  background: var(--color-danger-hover);
  border-color: var(--color-danger-hover);
}

.btn-sm {
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
}

/* Views write a row of buttons as a bare <p>. Make that a wrapping flex row so
   buttons keep their spacing when they drop onto a second line — inline-blocks
   would butt against each other vertically. Every such <p> in the app holds
   buttons only, never prose. */
p:has(> .btn) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

/* Forms */
.field {
  margin-bottom: var(--space-4);
}

.field label {
  display: block;
  font-weight: 600;
  font-size: var(--text-sm);
  margin-bottom: var(--space-1);
}

/* Setting taken over by a device profile: shown, but not the user's to change. */
.field-suppressed {
  opacity: 0.5;
}

.field-suppressed label,
.field-suppressed input {
  cursor: not-allowed;
}

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="url"],
.field input[type="number"],
.field textarea,
.field select {
  display: block;
  width: 100%;
  padding: var(--space-2);
  /* Never below 16px: iOS zooms the viewport when a smaller control takes
     focus, and it does not zoom back out. */
  font-size: var(--text-base);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-bg);
  color: var(--color-text);
  font-family: inherit;
}

.field textarea {
  resize: vertical;
}

.field .hint {
  font-size: var(--text-xs);
  color: var(--color-muted);
  margin-top: var(--space-1);
}

/* A page's opening paragraph: what this page is for, before the first control.
   Wider than a .field .hint and at reading size, because it is read once and
   has to carry more than a caption. */
.lede {
  max-width: var(--measure);
  margin-bottom: var(--space-5);
  color: var(--color-text-secondary);
}

.inline-field {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.inline-field label {
  margin-bottom: 0;
}

.inline-form {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.actions {
  margin-top: var(--space-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.form-errors {
  background: var(--rating-again-bg);
  border: 1px solid var(--rating-again-edge);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
  color: var(--color-danger);
}

.form-errors h3 {
  font-size: var(--text-sm);
  margin-bottom: var(--space-2);
}

.form-errors ul {
  font-size: var(--text-sm);
  margin-bottom: 0;
}

/* Tables
   A table with more columns than the screen has room for must scroll inside
   its own box; without the wrapper it widens the whole document and every
   other element scrolls sideways with it. Index views wrap their table in
   .table-wrap for this reason. */
.table-wrap {
  overflow-x: auto;
  margin-bottom: var(--space-5);
  -webkit-overflow-scrolling: touch;
  /* The panel. An index page was a bare table under a bare heading, with
     nothing to say where the data started and stopped; the border does that,
     and the overflow already clips the table to the rounded corners. */
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-bg);
}

.table-wrap > .table {
  margin-bottom: 0;
}

/* The panel supplies the outer edge, so the last row must not draw a second
   line just inside it. */
.table-wrap > .table tbody tr:last-child > * {
  border-bottom: 0;
}

.table {
  width: 100%;
  /* Below this the columns compress into unreadable slivers, so the wrapper
     scrolls instead. Above it the table simply fills its container and prose
     cells wrap as before. */
  min-width: 30rem;
  border-collapse: collapse;
  margin-bottom: var(--space-5);
}

.table th,
.table td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--text-sm);
}

/* Small capitalised labels rather than bold body text: the header row stops
   competing with the data and starts labelling it. */
.table th {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-text-secondary);
  background: var(--color-bg-alt);
  white-space: nowrap;
}

/* Filter/mode bar: one row of mutually exclusive choices, used by the Review
   sort-by bar and the folder state filters. */
.mode-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
}

.mode-bar-label {
  color: var(--color-text-secondary);
  /* Narrow: the label takes a line of its own so the five buckets form one
     clean row under it. Sharing the line puts two buckets beside the label and
     leaves the other three ragged below, which is the unreadable case. */
  flex: 1 0 100%;
}

.mode-bar-item {
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
}

.mode-bar-item.is-active {
  background: var(--color-primary);
  color: var(--color-on-accent);
  font-weight: 600;
}

/* Triage controls on a table row: a wrapping cluster so a narrow screen stacks
   them instead of stretching the column. */
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* The choice a row already sits in: present so the row reads as a set, but
   inert, and dimmed rather than recoloured so it needs no palette of its own. */
.row-actions .is-current {
  opacity: 0.5;
  cursor: default;
}

/* The mark toggle is a glyph, not a word: give it its own size and drop the
   link underline that would otherwise sit under a star. */
.mark-toggle {
  font-size: var(--text-lg);
  line-height: 1;
  text-decoration: none;
}

/* One-glyph flag column: it exists to be scanned down, not read across, so it
   takes only the width its glyph needs and centres it under its label. */
.table .col-mark {
  width: 3rem;
  text-align: center;
}

.table tbody tr:hover {
  background: var(--color-bg-alt);
}

/* Badges */
.badge {
  display: inline-block;
  padding: 0.125rem var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  border-radius: 9999px;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  white-space: nowrap;
}

/* Light values as fallbacks: only base.css's dark blocks define --badge-*. */
.badge-new { background: var(--badge-new-bg, #dbeafe); color: var(--badge-new-fg, #1e40af); border-color: var(--badge-new-edge, #93c5fd); }
.badge-learning { background: var(--badge-learning-bg, #fef3c7); color: var(--badge-learning-fg, #92400e); border-color: var(--badge-learning-edge, #fcd34d); }
.badge-review { background: var(--badge-review-bg, #d1fae5); color: var(--badge-review-fg, #065f46); border-color: var(--badge-review-edge, #6ee7b7); }
.badge-suspended { background: var(--badge-suspended-bg, #f3f4f6); color: var(--badge-suspended-fg, #6b7280); border-color: var(--badge-suspended-edge, #d1d5db); }
.badge-buried { background: var(--badge-buried-bg, #f3f4f6); color: var(--badge-buried-fg, #9ca3af); border-color: var(--badge-buried-edge, #e5e7eb); }

/* The count of marked cards under a note. A different shape from the note's
   own ★ on purpose: it is the one element on a note row that reports on
   another level, and it is read-only. */
.badge-cards-marked { background: var(--badge-cards-marked-bg, #ede9fe); color: var(--badge-cards-marked-fg, #5b21b6); border-color: var(--badge-cards-marked-edge, #c4b5fd); }

.badge-again { background: var(--rating-again-bg); color: var(--rating-again-fg); border-color: var(--rating-again-edge); }
.badge-hard { background: var(--rating-hard-bg); color: var(--rating-hard-fg); border-color: var(--rating-hard-edge); }
.badge-good { background: var(--rating-good-bg); color: var(--rating-good-fg); border-color: var(--rating-good-edge); }
.badge-easy { background: var(--rating-easy-bg); color: var(--rating-easy-fg); border-color: var(--rating-easy-edge); }

/* Flash messages */
/* The accent bar carries the notice/alert distinction at a glance, so the tint
   behind the text can stay quiet. */
.flash {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
  border-left: 3px solid transparent;
}

.flash-notice {
  background: var(--rating-good-bg);
  color: var(--rating-good-fg);
  border: 1px solid var(--rating-good-edge);
  border-left: 3px solid var(--color-success);
}

.flash-alert {
  background: var(--rating-again-bg);
  color: var(--rating-again-fg);
  border: 1px solid var(--rating-again-edge);
  border-left: 3px solid var(--color-danger);
}

/* Readonly copyable input */
.field input[readonly] {
  background: var(--color-bg-alt);
  cursor: text;
  color: var(--color-text-secondary);
}

/* Empty states. An index with nothing in it was a line of body text under a
   heading, which reads as a page that failed to load rather than as a list
   that is simply empty. Same panel as the table it stands in for. */
.empty-state {
  margin-bottom: var(--space-5);
  padding: var(--space-6) var(--space-4);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-lg);
  background: var(--color-bg-alt);
  color: var(--color-text-secondary);
  text-align: center;
  font-size: var(--text-sm);
}

/* Tag list */
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

/* Touch targets. A coarse pointer needs a 44px box; a mouse does not, and
   forcing one everywhere would loosen the desktop layout for nothing. */
@media (pointer: coarse) {
  .btn {
    min-height: var(--tap-target);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .btn-sm {
    min-height: 2.25rem;
  }

  .field input[type="text"],
  .field input[type="email"],
  .field input[type="password"],
  .field input[type="url"],
  .field input[type="number"],
  .field select {
    min-height: var(--tap-target);
  }
}
/* Navigation
   Nine sections and Sign out, one row that wraps when it must. On an Android
   phone the wrapping took four rows — a third of the screen before any
   content — so the portable presentation folds them into a menu instead (see
   the end of this file). */
.main-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--container-pad);
  background: var(--color-bg-alt);
  border-bottom: 1px solid var(--color-border);
}

.nav-brand {
  flex: none;
}

.nav-brand a {
  font-weight: 700;
  font-size: var(--text-lg);
  letter-spacing: -0.01em;
  color: var(--color-text);
  text-decoration: none;
}

.nav-links {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  align-items: baseline;
  /* Take the rest of the brand's row and wrap within itself, so the links stay
     a single block instead of interleaving with the brand. */
  flex: 1 1 auto;
}

.nav-links a {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  padding: var(--space-1) 0;
}

.nav-links a:hover {
  color: var(--color-primary);
  text-decoration: none;
}

.nav-links a.active {
  color: var(--color-primary);
  font-weight: 600;
  border-bottom: 2px solid var(--color-primary);
}

/* Container */
.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--container-pad);
}

/* Page header.
   Every page in this app opens the same way: an <h1>, usually followed by a
   row of buttons, then the content. Nothing marked where the header ended, so
   a title, its actions and the first table all read as one undifferentiated
   stack. One hairline fixes that — the trick is that it has to fall below the
   action row when there is one, and below the title when there is not.

   The second rule is written `:has(+ p > .btn)` and not `:has(+ p:has(> .btn))`
   because :has() may not be nested inside :has(). A browser drops the whole
   rule as invalid, and the page then draws both hairlines — under the title and
   under the buttons — instead of one. */
.container > h1 {
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.container > h1:has(+ p > .btn) {
  margin-bottom: var(--space-4);
  padding-bottom: 0;
  border-bottom: 0;
}

.container > h1 + p:has(> .btn) {
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

@media (min-width: 40rem) {
  .main-nav {
    gap: var(--space-2) var(--space-5);
  }

  .nav-links {
    gap: var(--space-4);
  }
}

/* On a touch screen the nav links are the smallest thing on the page and the
   easiest to miss. Give them a real target without changing the desktop
   density. */
@media (pointer: coarse) {
  .nav-links a {
    padding: var(--space-2) 0;
    min-height: var(--tap-target);
    display: inline-flex;
    align-items: center;
  }
}

/* Portable presentation: the folded menu.
   `body.portable` comes from the server (ApplicationHelper#portable_device?),
   not from a width query: the split is about the device the app is studied
   on. Folding was rejected at first because every change of section costs a
   second tap; seen on a phone, four rows of links above every page cost more
   (15 September 2026).

   The menu is a <details>, so it opens and closes without JavaScript and every
   new page arrives with it closed. Both link lists are on every page and this
   CSS shows one. The open list drops over the content instead of pushing it
   down, so opening the menu does not move the page.

   The bar sticks to the top of the viewport. A phone's lists run long, and a
   nav that scrolls away costs a trip back to the top of the page before any
   change of section — the second tap folding already charges, paid twice.
   `sticky` and not `fixed` so the bar still reserves its own row and nothing
   has to be padded out from under it; being positioned, it is also what the
   open list is placed against, which is the job `relative` used to do. */
.nav-section,
.nav-menu {
  display: none;
}

.portable .main-nav {
  position: sticky;
  top: 0;
  z-index: 20;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: space-between;
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
}

.portable .nav-links {
  display: none;
}

/* A long section name is cut short rather than pushing the button off the
   row. */
.portable .nav-brand {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.portable .nav-section {
  display: inline;
  margin-left: var(--space-1);
  color: var(--color-text-secondary);
}

.portable .nav-menu {
  display: block;
  flex: none;
}

.nav-menu > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-target);
  padding: 0 var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  background: var(--color-bg);
  font-size: var(--text-sm);
  color: var(--color-text);
  cursor: pointer;
  list-style: none;
  user-select: none;
}

/* Safari draws the disclosure triangle through its own pseudo-element and
   ignores list-style. */
.nav-menu > summary::-webkit-details-marker {
  display: none;
}

/* The two glyphs have different advance widths, so the box is sized once and
   the glyph centred in it: the button must not change width as it opens. */
.nav-menu > summary::before {
  content: "☰";
  flex: none;
  width: 1em;
  text-align: center;
}

.nav-menu[open] > summary::before {
  content: "✕";
}

/* The panel is white on a white page, so its edge is the only thing saying it
   floats rather than that the page moved down. --shadow is the resting shadow
   for a panel sitting in the page and is far too faint for that; this one is
   cast by something above it. The cap is a safety net: ten rows at --tap-target
   fit a phone today, but a taller menu must scroll inside itself rather than
   run off the bottom with no way back. */
.nav-menu-links {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 10;
  max-height: 80vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border-strong);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
}

.nav-menu-links a {
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
  padding: 0 var(--container-pad);
  color: var(--color-text);
}

.nav-menu-links a + a {
  border-top: 1px solid var(--color-border);
}

.nav-menu-links a:hover {
  text-decoration: none;
}

.nav-menu-links a.active {
  color: var(--color-primary);
  font-weight: 600;
}

/* Portable session surfaces: /study, its Review twin and the reading pages
   behind them (ApplicationHelper#session_surface?). These are a loop, not a
   place you navigate from — the e-ink client already draws its session pages
   without navigation, and on a phone that row is the most expensive on a
   screen whose scarce dimension is height. The way out is the "Back to
   Collection" button at the foot of the page; a phone scrolls, so it does not
   need the e-ink client's top-right closer.

   `.portable` scopes all of this to Android on purpose. The desktop keeps its
   nav until the desktop's own visual pass decides otherwise — that is a
   separate task, and a page whose structure differs by device for no stated
   reason is worse than either choice made deliberately. */
.portable.session-surface .main-nav {
  display: none;
}

/* The note reading surface: shared/_note_reading, rendered by the study
   reading page and by the folder reader. The embryo of the wiki page, so the
   rules here are about prose rather than about either of its two callers. */
.note-reader {
  max-width: var(--measure);
  margin: 0 auto var(--space-6);
  /* One multiplier for the whole page rather than one per element: a text page
     is continuous prose, and what is wanted of it is larger or smaller, not the
     title re-proportioned against the body (web_android.md §4.1). Unset — every
     surface but the Android study terminal — it is 1 and this reads as 1rem. */
  font-size: calc(1rem * var(--portable-reading-size, 1));
}

/* The title scales with the body, which means restating base.css's own h1 clamp
   here so it can be multiplied. Duplicated values, deliberately: expressing the
   title in `em` of the scaled body instead would collapse the clamp to its
   floor and shrink the title on any screen wide enough to have grown it. Keep
   the three numbers in step with base.css. */
.reader-title {
  font-size: calc(clamp(1.5rem, 1.28rem + 1.1vw, 1.75rem) * var(--portable-reading-size, 1));
  margin-bottom: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

.reader-body p {
  margin-bottom: var(--space-4);
}

.reader-empty {
  color: var(--color-text-secondary);
}

/* Study's return path out of the reading page. The field name sits beside the
   back link because `example` and `extra` are otherwise indistinguishable once
   you have landed. */
.reading-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  max-width: var(--measure);
  margin: 0 auto var(--space-5);
}

.reading-field {
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
}

/* Reader navigation on the folder pages. */
.reader-nav,
.reader-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
}

.reader-actions {
  margin: var(--space-5) 0 0;
}

.btn.disabled {
  color: var(--color-muted);
  cursor: default;
}
/* Stat cards. The headline figure is the point of these, so the number is
   given the display size from the scale and the label is demoted to a small
   capitalised caption — the same label language as a table heading. */
.stats-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.75rem, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.stat-card {
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  text-align: center;
}

.stat-number {
  display: block;
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--color-primary);
  line-height: 1.2;
  /* Figures in a row of cards must not jiggle as the digits change. */
  font-variant-numeric: tabular-nums;
}

.stat-label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--color-text-secondary);
  margin-top: var(--space-1);
}
/* Title, sort bar and counts, grouped. The group exists for the phone (below),
   but grouping moved the <h1> out of reach of layout.css's `.container > h1`,
   which is where it took its hairline and margins from. These three lines are
   that rule restated where the title now lives, so that the desktop sees
   exactly what it saw before the wrapper existed. */
.session-head > h1 {
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

/* Study page */
.study-counts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-5);
  font-size: var(--text-sm);
}

.count-new { color: var(--color-primary); font-weight: 600; }
.count-learning { color: var(--color-warning); font-weight: 600; }
.count-due { color: var(--color-success); font-weight: 600; }

/* Flashcard */
.flashcard {
  /* Was a fixed 600px. The card holds short prose, so it wants the same measure
     as the reading page — and expressed in rem it grows with a reader who has
     raised their browser's default font size, which a pixel value did not. */
  max-width: var(--measure);
  margin: var(--space-4) auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  /* 2rem of padding is a fifth of a phone's width. Narrow screens get it back
     and spend it on the content. */
  padding: var(--space-4);
  text-align: center;
  /* The same white as the page in light mode, so this changes nothing there.
     On the dark terminal it is what lifts the card off the ground, since the
     shadow cannot. */
  background: var(--color-surface);
  box-shadow: var(--shadow);
}

/* The question is the focal point of the page, so it keeps a size of its own
   rather than deferring to the fluid heading scale. */
.flashcard-front h2 {
  font-size: calc(var(--text-xl) * var(--portable-question-size, 1));
  font-weight: var(--portable-question-weight, 700);
  margin-bottom: var(--space-3);
}

.flashcard-front .hint {
  color: var(--color-muted);
  font-style: var(--portable-hint-style, italic);
  font-size: calc(var(--text-sm) * var(--portable-hint-size, 1));
}

.flashcard-back {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

/* The answer keeps the weight and size it had as an <h3> — it is the payoff of
   the card and is meant to land like one — and drops the two things that came
   with the heading and were not wanted:

   - the heading line-height. base.css sets 1.3 on h1-h6, which is right for a
     title and tight for an answer running to several lines; a <p> inherits the
     body's 1.6.
   - the centring, but only when it costs something. `display: table` shrinks
     the box to its own text, so the auto margins still centre a short answer
     while a long one fills the measure and reads from the left. Neither case
     has to be detected.

   The question above keeps plain centring: it is short by construction. */
.flashcard-answer {
  display: table;
  margin: 0 auto var(--space-2);
  font-size: calc(var(--text-lg) * var(--portable-answer-size, 1));
  font-weight: var(--portable-answer-weight, 700);
  text-align: left;
}

.flashcard-actions {
  margin-top: var(--space-5);
}

/* Rating buttons. The grid goes on the form, not on .rating-buttons, because
   the four buttons are the form's children — and .rating-buttons must keep no
   display rule of its own, since it is the element the reveal hides. */
.rating-buttons form {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2);
}

.btn-rating {
  /* The one control on this page you press dozens of times a session, so it
     gets a full touch target whatever the pointer is. The app-wide rule in
     components.css reserves that for coarse pointers, which is right for
     buttons you meet once. Equal columns do the sizing, so the horizontal
     padding only has to keep the label off the edges. */
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-target);
  padding: var(--space-2) var(--space-3);
  font-weight: 600;
  border-radius: var(--radius);
}

/* The rating palette lives in base.css and is shared with the history badges,
   so a rating means the same colour wherever you meet it. Hover darkens the
   tint by borrowing the border colour at low opacity rather than naming four
   more hexes. */
.btn-again {
  background: var(--rating-again-bg);
  color: var(--rating-again-fg);
  border-color: var(--rating-again-edge);
}

.btn-hard {
  background: var(--rating-hard-bg);
  color: var(--rating-hard-fg);
  border-color: var(--rating-hard-edge);
}

.btn-good {
  background: var(--rating-good-bg);
  color: var(--rating-good-fg);
  border-color: var(--rating-good-edge);
}

.btn-easy {
  background: var(--rating-easy-bg);
  color: var(--rating-easy-fg);
  border-color: var(--rating-easy-edge);
}

.btn-again:hover { border-color: var(--rating-again-fg); }
.btn-hard:hover  { border-color: var(--rating-hard-fg); }
.btn-good:hover  { border-color: var(--rating-good-fg); }
.btn-easy:hover  { border-color: var(--rating-easy-fg); }

.study-complete {
  text-align: center;
  padding: var(--space-7) var(--space-4);
}

.study-complete h2 {
  color: var(--color-success);
}

/* Sort-by bar (Review only). The web equivalent of the e-ink study_mode page:
   a persistent menu is affordable here, so the buckets stay on the page with
   the active one marked instead of living behind a separate screen. */
.count-mode { color: var(--color-text-secondary); }
.count-left { color: var(--color-primary); font-weight: 600; }

/* Auxiliary actions under the card. Writes, so each is a form rather than a
   link — but they read as the same quiet row of verbs as on the e-ink client. */
.card-aux-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.card-aux-nav form {
  display: inline;
  margin: 0;
}

.aux-link {
  background: none;
  border: none;
  padding: var(--space-1);
  /* `font: inherit` is a shorthand and resets weight, so both overrides have
     to come after it. */
  font: inherit;
  font-size: calc(var(--text-sm) * var(--portable-aux-size, 1));
  font-weight: var(--portable-aux-weight, 400);
  color: var(--color-text-secondary);
  cursor: pointer;
}

.aux-link:hover {
  color: var(--color-primary);
  text-decoration: underline;
}

/* A field this note does not have: present, so the row keeps its shape, but
   plainly not a target. */
.aux-link.is-disabled {
  color: var(--color-muted);
  cursor: default;
}

.aux-link.is-disabled:hover {
  color: var(--color-muted);
  text-decoration: none;
}

/* Roomy and up: the four ratings fit across, and the sort label rejoins its
   row. */
@media (min-width: 40rem) {
  .rating-buttons form {
    grid-template-columns: repeat(4, 1fr);
  }

  .mode-bar-label {
    flex: 0 0 auto;
  }

  .flashcard {
    padding: var(--space-6);
  }
}

/* Review says which bucket it is sorting by twice: the bar's "Sort by:" label
   names the row, and .count-mode restates the active one as "Sort: unknown"
   just below it — which the filled blue pill has already said, unmistakably and
   in the right place. Both go on a phone, leaving the five buckets and the
   count of what is left.

   The label is the one that costs real height: components.css gives it a line
   of its own (~30px) because at this width it cannot share one. .count-mode
   costs no line, since "N left" holds that row anyway; it goes because it is
   noise in a row of counts.

   ⚠️ Hidden on `.portable`, not deleted: both still render on the desktop,
   which has its own visual pass coming (web_visual_identity.md) and is where
   the question of whether they are wanted belongs. Noted here only so that an
   element appearing on one device and not another is not a mystery. */
.portable.session-surface .mode-bar-label,
.portable.session-surface .count-mode {
  display: none;
}

/* The ratings are the one control on this page pressed dozens of times a
   session, and on a phone the one pressed without looking closely at it.
   --tap-target is the floor for a comfortable target, not a generous one; a
   thumb aiming at one of four adjacent buttons gets more than the floor.

   The gap grows with the height because a mis-tap here is not a visual error.
   In Study a wrong rating reschedules the card and appends a review_logs row,
   and that row asserts something about an attempt at recall — history, which
   is not corrected afterwards without lying about it. 4px of gap is a cheap
   price for making the wrong neighbour harder to hit.

   Both are set here rather than by moving --tap-target, which is the app-wide
   floor and is right where it is. */
.portable.session-surface .btn-rating {
  min-height: 3.5rem;
}

.portable.session-surface .rating-buttons form {
  gap: var(--space-3);
}

/* The four ratings on one row, taller, on a phone.
   This is not the compromise it looks like. What decides whether a thumb hits
   the button it aimed at is the shorter of the two sides, and one row improves
   that while also giving a whole row of height back:

     2x2, as the desktop's narrow case leaves it   143 x 44   short side 44px
     1x4 at 56px tall                               67 x 56   short side 56px

   96px of ratings become 56px, and every target gets easier to hit. The four
   also read as what they are — one scale from Again to Easy — which two lines
   break. And the desktop is already 4-across above 40rem, so this agrees with
   it rather than inventing a third arrangement.

   The gap stays what the base rule sets, so only the columns are overridden: a
   narrower gap widens the buttons, which is what "Again" needs.

   The horizontal padding has to come down with the columns: at ~67px a column,
   "Again" at --text-sm and 600 needs roughly 40px for the word and cannot
   afford 12px of padding on each side of it. The vertical padding is irrelevant
   now that min-height sets the size, so it goes to 0 and lets the flex centring
   place the label. */
.portable.session-surface .rating-buttons form {
  grid-template-columns: repeat(4, 1fr);
}

.portable.session-surface .btn-rating {
  min-height: 3.5rem;
  padding: 0 var(--space-1);
}

/* The band the nav used to be.
   Hiding the nav on a phone took the page's top edge with it, and the title,
   the buckets and the count were left against nothing — which reads as their
   having been shoved upwards rather than as space. So the header takes over the
   nav's own ground: the same --color-bg-alt, the same hairline under it, the
   same full width. The page keeps the top it always had; only what it says
   changes.

   A bordered white panel was the other candidate and was rejected before
   building: next to the flashcard's own frame it reads as a second card and
   invites the question of why there are two, and it costs more height. A band
   is chrome rather than content, so it does not compete with the card.

   The negative margins cancel .container's padding so the band runs edge to
   edge and starts where the nav started. On a screen wider than --container-max
   they reach the content column's edges rather than the window's, which is the
   tablet case and is fine. */
.portable.session-surface .session-head {
  margin: calc(-1 * var(--container-pad)) calc(-1 * var(--container-pad)) var(--space-5);
  padding: var(--space-3) var(--container-pad);
  background: var(--color-bg-alt);
  border-bottom: 1px solid var(--color-border);
}

/* Inside the band the title is a header, not a page title introducing content
   below a rule — the band's own hairline does that job now. */
.portable.session-surface .session-head > h1 {
  font-size: var(--text-lg);
  margin-bottom: var(--space-2);
  padding-bottom: 0;
  border-bottom: 0;
}

/* Whatever ends the band sets how deep it is, so its own bottom margin would
   pad the band rather than separate it from anything. */
.portable.session-surface .session-head > :last-child {
  margin-bottom: 0;
}

/* The secondary controls. Unlike the ratings these are worth a target only
   where a finger is doing the pointing. */
@media (pointer: coarse) {
  .aux-link,
  .mode-bar-item {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap-target);
  }

  .aux-link {
    padding-left: var(--space-2);
    padding-right: var(--space-2);
  }
}
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *


 */
