:root {
  --bg: #f4f7fb;
  --ink: #152238;
  --muted: #5a6b83;
  --nav: #0f1f37;
  --accent: #4656e8;
  --accent-dark: #2638e4;
  /* Fix-wave F1: a SEPARATE token from --accent for focus-ring color.
     WCAG 1.4.11 (non-text contrast, >= 3:1) applies to the SAME outline on
     BOTH light surfaces (page background/.card, where --accent itself
     clears 3:1) AND dark surfaces (.topbar/.sidebar, --nav: #0f1f37 /
     #1d3355) - where --accent no longer does, now that IR-019 deepened it
     to #4656e8 (2.95:1 on --nav, 2.27:1 on #1d3355 - both under 3:1). This
     token defaults to --accent for light surfaces; .topbar/.sidebar/
     .nav-toggle-label below override it to white via ordinary CSS custom-
     property inheritance - no duplicated selector list, no !important. */
  --focus: var(--accent);
  --card: #ffffff;
  --border: #dbe3ef;
  --success: #e7f7ee;
  --danger: #b42318;
  --danger-bg: #fdecea;
  --danger-border: #f3c2b8;
  /* S8-08/IR-085: the two message.tags this app has always been able to
     flash (messages.warning - 2 call sites; messages.info - 1 call site)
     never had a design token at all, unlike success/danger above - the
     flash shell threw message.tags away entirely, so nothing ever needed
     one until now. Chosen to sit clearly apart, in hue, from BOTH
     --success (green) and --danger (red) and from --accent (indigo/blue-
     purple, the link/button color) so a warning or info flash is never
     mistaken for either: amber for warning (the conventional "caution"
     hue, and colorblind-distinguishable from red/green), a cooler blue
     for info than --accent's own purple-leaning blue. Every one of the
     three (text-on-own-background, >= 4.5:1) and (text/warning/info
     border vs the page --bg actually behind it) contrast pairs is measured
     in test_a11y.py's MessageSeverityContrastAndRoleContractTests - see that
     class's own docstring for the exact ratios and for why the BORDER
     figure is reported but not gated (the existing --success/--danger
     borders already sit under 3:1 against this same near-white --bg, a
     pre-existing condition of this soft-pastel token family, not
     something this task's new tokens could fix in isolation without
     redesigning --success/--danger too - out of this task's scope). */
  --warning-bg: #fff3d6;
  --warning-border: #e8c877;
  --warning: #6b3e00;
  --info-bg: #e3eefb;
  --info-border: #aecdec;
  --info: #0a4a80;
  --shadow: 0 16px 40px rgba(15, 31, 55, 0.08);
}

* { box-sizing: border-box; }
/* IR-042 fix (S8-02, refined in the M7 fix-wave): no rule anywhere in this
   file bounded how wide a single unbroken run of characters (no spaces to
   break at - e.g. a long AI-generated or user-typed Pack/Opportunity/
   Source title) could render, so ONE such title pushed the whole page far
   wider than the viewport on both desktop and mobile (measured before/
   after in test_s8_02.py's own LongTitleNoHorizontalOverflowContractTests).
   overflow-wrap: anywhere lets the browser break such a run at ANY
   character once it no longer fits - unlike the older word-wrap:
   break-word, it also counts toward min-content sizing, which is what lets
   a long title shrink a flex item (e.g. .source-row's own children) back
   down instead of forcing the row wider than its container. A normal,
   space-separated sentence is completely unaffected: the browser still
   prefers breaking at a space first, exactly as before this rule.

   M7 fix-wave (code-reviewer finding): the ORIGINAL version of this fix
   targeted only h1/h2/h3 plus ".source-row strong"/".opportunity-row
   strong" - it missed opportunities.html's own "<li>{{ source.title }}"
   list items and exports/detail.html's own "<p>{{ source.title }}"
   paragraph (neither is an h1/h2/h3 or a `<strong>`), and
   ".opportunity-row" itself turned out to be a DEAD selector (grepping
   every template under editorial/templates finds zero elements with that
   class - only studio.html/today.html/several others use ".source-row").
   overflow-wrap is an INHERITED property, so setting it once here, on
   `body`, reaches every title-bearing element in the app - present AND
   future - without hand-listing each one; this replaces (does not add
   to) that narrower, incomplete selector list. */
body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background: var(--bg); color: var(--ink); overflow-wrap: anywhere; }
a { color: var(--accent); }
button, input, select, textarea { font: inherit; }

/* Keyboard focus must always stay visible: one global rule, never suppressed
   with outline:none/0 anywhere else in this file. Uses --focus (NOT
   --accent directly, fix-wave F1) so this exact rule renders correctly on
   both light and dark surfaces - see --focus's own definition above. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* Skip link: kept in normal flow (not display:none) so it stays reachable by
   keyboard/AT even while parked off-screen; slides into view on focus. */
.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  background: #fff;
  color: var(--ink);
  padding: 10px 18px;
  border-radius: 8px;
  border: 2px solid var(--accent);
  font-weight: 750;
  text-decoration: none;
  z-index: 1000;
  transition: top 0.15s ease-in-out;
}
.skip-link:focus {
  top: 12px;
}
/* Fix-wave G3: .skip-link is a SIBLING of .app-shell (not a descendant of
   .topbar/.sidebar), so it never inherits their dark-surface --focus
   override above - it keeps :root's light-surface default (var(--accent)).
   On every authenticated core page (all of which render .app-shell -
   base.html's own structure), .skip-link's focused position (top: 12px)
   geometrically overlaps .topbar's own rendered area on this page (this
   fix's own real-browser regression test asserts the focused+outline
   bounding box sits entirely within .topbar's, gating the contrast
   comparison on that overlap) - var(--accent) there falls to ~2.95:1
   against --nav, under WCAG 1.4.11's 3:1 floor. `:has()` scopes the
   override to EXACTLY the pages where that overlap is real: unauthenticated
   login/signup pages never render .app-shell, so this selector never
   matches there and --focus stays at the light-surface default those pages
   need (the "mirror regression" a blanket white override would otherwise
   cause) - both states are pinned by this fix's own real-browser tests. */
body:has(.app-shell) .skip-link {
  --focus: #ffffff;
}

.app-shell {
  display: grid;
  grid-template-columns: 248px 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "header header"
    "sidebar main";
  min-height: 100vh;
}
.topbar {
  grid-area: header;
  background: var(--nav);
  color: #fff;
  display: flex;
  align-items: center;
  padding: 18px 24px;
  /* Fix-wave F1: this is a dark surface - override --focus to white for
     every focus-visible control inside (e.g. .brand), via ordinary custom-
     property inheritance (see --focus's own :root definition). */
  --focus: #ffffff;
}
.sidebar {
  grid-area: sidebar;
  position: relative;
  background: var(--nav);
  color: #fff;
  padding: 28px 20px;
  display: flex;
  flex-direction: column;
  /* Fix-wave F1: same dark-surface override as .topbar above - covers the
     sidebar nav links AND (via inheritance) .nav-toggle-label nested
     inside it. */
  --focus: #ffffff;
}
.brand { color: #fff; text-decoration: none; font-size: 22px; font-weight: 850; }

/* Mobile navigation toggle: CSS-only (checkbox + label), no JS/onclick.
   The checkbox is visually clipped (not display:none/visibility:hidden) so
   it stays focusable and announced by assistive tech; the label is what
   looks like a button. Hidden entirely above the mobile breakpoint. */
.nav-toggle-input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
}
.nav-toggle-input:focus-visible + .nav-toggle-label {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}
/* Fix-wave F1: explicit --focus override directly on .nav-toggle-label
   too (belt-and-suspenders alongside .sidebar's own override above,
   which already inherits down to this element in the real DOM - see
   base.html) so this rule stays correct even if the label's markup
   position ever moves. */
.nav-toggle-label { display: none; --focus: #ffffff; }

.sidebar nav { display: grid; gap: 8px; }
.sidebar nav a { color: #c8d4e8; text-decoration: none; padding: 12px 14px; border-radius: 10px; }
.sidebar nav a:hover, .sidebar nav a:focus-visible { background: #1d3355; color: #fff; }
/* M10 fix-wave (code-reviewer finding): base.html's own second <nav>
   (IR-041) is the LAST child of .sidebar in DOM order - deliberately, so
   the forbidden tests/accessibility/test_a11y.py Tab-order test (skip-link
   -> .topbar brand -> the 4 #primary-navigation links -> .sidebar-footer's
   own logout button, exactly 7 presses) keeps landing on the same 7
   elements in the same order; these 4 new links only become reachable
   from Tab 8 onward. Left at its plain DOM position, that also means it
   renders BELOW .sidebar-footer (whose own `margin-top: auto` pushes it
   to the very bottom) - visually below the logout button, which reads as
   an odd, disconnected afterthought. `order` (Flexbox - .sidebar is
   `display: flex; flex-direction: column`) reorders the VISUAL layout
   only; it has no effect on DOM order, source order, or Tab order (all
   three are governed by document position, never by `order` - verified
   against tests/accessibility/test_a11y.py's own Tab-count test, which
   stays green with this change). `.secondary-nav` therefore renders
   ABOVE `.sidebar-footer` (grouping it with the other nav links, above
   the account/logout area at the very bottom, matching the same visual
   position #primary-navigation itself would imply) while staying its
   LAST DOM child underneath. */
.secondary-nav { order: 1; }
.sidebar-footer { order: 2; margin-top: auto; display: grid; gap: 8px; color: #aebbd0; font-size: 13px; }
.link-button { appearance: none; border: 0; padding: 0; background: transparent; color: #dce6f7; cursor: pointer; text-align: left; }
.main-content { grid-area: main; padding: 36px; max-width: 1280px; width: 100%; }
.page-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; }
h1 { font-size: clamp(30px, 4vw, 42px); letter-spacing: -0.03em; margin: 0 0 8px; }
h2 { margin: 0 0 12px; }
.eyebrow { margin: 0 0 6px; color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.muted { color: var(--muted); }
.small { font-size: 13px; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 18px; padding: 24px; margin-top: 24px; box-shadow: var(--shadow); }
.card-heading { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; }
.metrics-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 24px; }
.metric-card { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 20px; }
.metric-card span { color: var(--muted); display: block; }
.metric-card strong { display: block; font-size: 34px; margin-top: 8px; }
.two-column { display: grid; grid-template-columns: minmax(280px, 0.8fr) minmax(0, 1.2fr); gap: 24px; }
.stack-form { display: grid; gap: 14px; }
.stack-form p { display: grid; gap: 6px; margin: 0; }
.stack-form input, .stack-form select, .stack-form textarea { width: 100%; border: 1px solid var(--border); border-radius: 10px; padding: 11px 12px; background: #fff; }
.stack-form .helptext { color: var(--muted); font-size: 12px; }
.errorlist { color: var(--danger); margin: 0; padding-left: 20px; }
.button { display: inline-flex; justify-content: center; align-items: center; border: 0; border-radius: 10px; padding: 11px 16px; text-decoration: none; font-weight: 750; cursor: pointer; }
.button.primary { background: var(--accent); color: #fff; }
.button.primary:hover { background: var(--accent-dark); }
/* IR-020 (S7-02): a disabled CTA (editorial/today.html's "次の一歩" card
   renders span.button.primary[aria-disabled="true"] whenever
   NextAction.cta_url_name is empty) carried zero [aria-disabled]/:disabled
   rule anywhere in this file before this addition, so it inherited the
   exact same solid --accent background/white text as a real, clickable
   .button.primary sitting right next to it in the same card - no visual
   signal to a sighted user that clicking would do nothing. Reuses existing
   tokens only: --bg/--muted is the SAME pair already proven >= 4.5:1
   together (test_a11y.py's .page-header .muted checks, against this exact
   background).

   Fix-wave H4 (code-reviewer/Codex #1, measured), figure corrected by
   fix-wave J2 (test-engineer - the figure below used to read "1.01:1",
   which is actually a DIFFERENT, unrelated comparison: this rule's own
   --bg fill against .next-action's #f6f8ff, not the border against
   either): the border was originally --border (#dbe3ef), which measured
   1.20:1 against this rule's own --bg and 1.22:1 against .next-action's
   own #f6f8ff - this comment used to claim a "visible edge" that, measured,
   was not one (the disabled button visually vanished into its card).
   --muted clears >= 3:1 against BOTH of the border's own real backgrounds
   (5.06:1 / 5.12:1 respectively - test_s7_02.py's own
   DisabledNextActionCtaContractTests.
   test_disabled_cta_border_meets_nontext_aa_against_its_two_real_
   backgrounds pins this, and fix-wave J3 closed a gap there: deleting this
   `border` declaration entirely used to still pass that test, since
   .button's own `border: 0` makes the computed borderColor resolve to
   currentColor - the disabled span's OWN text color, --muted - giving a
   passing ratio with NO border actually rendered; the test now also
   requires a real border-style/border-width), so the border now reuses
   --muted (no new literal color) instead.

   Fix-wave H1 (Codex #2, measured), corrected by fix-wave J1
   (code-reviewer - this note used to give .button.primary:hover TWO
   DIFFERENT specificity numbers in the same comment, self-contradictory
   and wrong: a pseudo-CLASS like :hover counts the same as a plain class
   toward specificity, W3C Selectors Level 4 section 17):

       .button.primary                              (0,2,0)
       .button.primary:hover                        (0,3,0)
       .button.primary[aria-disabled="true"]        (0,3,0)  <- TIES with
           :hover above; wins ONLY because it sits LATER in this file
       .button.primary[aria-disabled="true"]:hover  (0,4,0)  <- STRICTLY
           higher; the ONLY one of this pair whose win does not depend on
           file order

   So the base rule (no :hover) is NOT order-independent on its own -
   moving it before .button.primary:hover would put it back into a tie it
   can only lose there. The dedicated :hover entry below is what actually
   delivers "wins by specificity alone, regardless of where this block
   sits" (verified: every test_s7_02.py test still passed with this whole
   block temporarily moved to BEFORE .button.primary) - it can look
   redundant next to the base rule's own `background: var(--bg)` (today,
   deleting ONLY these two :hover lines does not itself regress, because
   the base rule's tie-plus-order win happens to cover the hover case too
   in this file's CURRENT arrangement - fix-wave J5's injection-based test,
   test_s7_02.py, closes exactly that blind spot by adding a SECOND, later
   .button.primary:hover rule at runtime) - do not delete it as a
   simplification; it is the only guarantee here that does not depend on
   this file's own arrangement. It also exists so mousing over a disabled
   control never shows the primary button's own --accent-dark highlight -
   fix-wave H1 found this had ZERO test coverage before; see test_s7_02.py's
   own hover tests.

   Fix-wave J7 (code-reviewer): narrowing the selector from the original
   .button[aria-disabled="true"] to .button.primary[aria-disabled="true"]
   (needed for the specificity fix above) is a deliberate trade-off, not an
   oversight - a future disabled CTA that is not also .primary (e.g. a
   secondary/tertiary disabled button) gets NONE of this rule's styling
   (background/color/border/cursor) until a matching rule is added for it
   too. */
.button.primary[aria-disabled="true"] {
  background: var(--bg);
  color: var(--muted);
  border: 1px solid var(--muted);
  cursor: not-allowed;
}
.button.primary[aria-disabled="true"]:hover {
  background: var(--bg);
}
.status-pill, .level-badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 6px 10px; background: #eef1ff; color: var(--accent); font-size: 12px; font-weight: 800; }
.list-stack { display: grid; gap: 12px; }
.source-row, .opportunity-row { display: flex; justify-content: space-between; gap: 16px; align-items: center; padding: 16px; border: 1px solid var(--border); border-radius: 14px; }
.source-row p, .opportunity-row p { margin: 4px 0 0; }
.empty-state { text-align: center; padding: 36px 18px; border: 1px dashed #b8c6da; border-radius: 14px; color: var(--muted); }
.empty-state strong { display: block; color: var(--ink); }
.next-action { display: grid; gap: 4px; background: #f6f8ff; border-radius: 14px; padding: 16px; margin-top: 20px; }
/* S8-08/IR-085: a failure must not render in the success color. Every
   messages.error/success/warning/info call (base.html now renders
   message.tags as this element's own class - see that template's own
   comment) gets its own background/border/text color instead of the ONE
   unconditional style this rule used to be (background: var(--success)
   for every severity, including the 64 messages.error call sites this
   product actually has - measured by the integrator, docs/evidence/
   s8-07/13-Uploads-取り込み-失敗.png). ".message" itself now only carries
   the STRUCTURAL shape (border-box/radius/padding/layout) - every color
   lives in the four ".message.<tag>" rules below, so a tag this dict does
   not recognize (never happens in practice - Django's own five level tags
   are fixed, and settings.py sets no MESSAGE_TAGS override) still renders
   a correctly-shaped, if uncolored (transparent border, inherited text
   color), box rather than an unstyled one. gap gives two stacked messages
   (e.g. a success immediately followed by a warning - views/uploads.py's
   _flash_ingest_outcome does exactly this for a completed upload with
   sensitive-content flags) daylight between their now-DIFFERENT-colored
   borders, which touching borders of the same green used to hide. */
.messages { margin-bottom: 16px; display: grid; gap: 8px; }
.message {
  display: flex;
  gap: 8px;
  align-items: baseline;
  border: 1px solid transparent;
  border-radius: 10px;
  padding: 12px 14px;
}
.message-label { flex-shrink: 0; font-weight: 800; }
.message.success { background: var(--success); border-color: #b7e3c9; }
.message.error { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger); }
.message.warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning); }
.message.info { background: var(--info-bg); border-color: var(--info-border); color: var(--info); }
.auth-shell { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at top, #edf0ff, var(--bg) 56%); }
.auth-card { width: min(100%, 460px); background: #fff; border: 1px solid var(--border); border-radius: 22px; padding: 32px; box-shadow: var(--shadow); }

/* Reusable state components (editorial/components/_loading.html,
   _empty.html reuses .empty-state above, _error.html). */
.state-loading {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 20px;
  color: var(--muted);
}
.spinner {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
.state-error {
  display: grid;
  gap: 4px;
  padding: 16px 18px;
  border: 1px solid var(--danger-border);
  background: var(--danger-bg);
  border-radius: 12px;
  color: var(--danger);
}
.state-error strong { color: #8a1a10; }

@media (max-width: 850px) {
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto 1fr;
    grid-template-areas:
      "header"
      "sidebar"
      "main";
  }
  .topbar { padding: 14px 18px; }
  /* IR-022 fix-wave H3 (code-reviewer/Codex #4, measured): .topbar's own
     rendered height shrinks to ~58px at this breakpoint (padding 14px vs
     18px/24px desktop) - .skip-link:focus's fixed top: 12px (with its own
     3px outline + 2px outline-offset expanding the visible ring 5px beyond
     its border box) put the ring's bottom edge at ~62px, 4px past
     .topbar's own bottom edge here - the ring's last few pixels sat on the
     PAGE background (--bg) instead of .topbar's (--nav), an under-3:1
     pairing no test caught (the old intersects-only check tolerated any
     partial overlap, not just full containment). Scoped to
     body:has(.app-shell) only (mirrors the .skip-link --focus override
     above) - the unauthenticated login/signup page never renders .topbar
     at all, so it has nothing to stay inside of and keeps its own
     unrelated top: 12px untouched.

     Fix-wave J6 (Codex #4, recorded not fixed - flagged for owner
     decision, not a regression this task closes): this top: 6px is a
     fixed value sized for this breakpoint's own ~58px .topbar height. At
     an extremely narrow width where "メインコンテンツへスキップ" itself
     wraps to two lines, .skip-link grows taller - since it is absolutely
     positioned, .topbar's own height does not grow to match, and the ring
     could again extend past .topbar's bottom edge exactly like the defect
     this fix-wave closed. Containment at such widths is NOT covered or
     guaranteed by this rule or by test_a11y.py's own mobile test (both are
     scoped to the harness's fixed 390px viewport). */
  body:has(.app-shell) .skip-link:focus {
    top: 6px;
  }
  .sidebar { padding: 14px 18px; }
  .nav-toggle-label {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    color: #fff;
    background: #1d3355;
    border-radius: 10px;
    padding: 10px 14px;
    font-weight: 750;
    width: fit-content;
  }
  .nav-toggle-icon,
  .nav-toggle-icon::before,
  .nav-toggle-icon::after {
    width: 18px;
    height: 2px;
    background: #fff;
  }
  .nav-toggle-icon {
    position: relative;
    display: inline-block;
  }
  .nav-toggle-icon::before,
  .nav-toggle-icon::after {
    content: "";
    position: absolute;
    left: 0;
  }
  .nav-toggle-icon::before { top: -6px; }
  .nav-toggle-icon::after { top: 6px; }
  .sidebar nav,
  .sidebar-footer {
    display: none;
    margin-top: 14px;
  }
  .nav-toggle-input:checked ~ nav,
  .nav-toggle-input:checked ~ .sidebar-footer {
    display: grid;
  }
  .main-content { padding: 24px 18px; }
  .metrics-grid, .two-column { grid-template-columns: 1fr; }
  .page-header { flex-direction: column; }
}

@media (min-width: 851px) {
  .nav-toggle-input,
  .nav-toggle-label {
    display: none;
  }
}
