/* quantsec.pro frontend - single stylesheet, no external fonts or assets.
 *
 * Light is the default. Dark applies in two cases: the visitor's OS asks for it and
 * they have not chosen otherwise, or they picked it explicitly with the toggle. Both
 * are token-only overrides - every rule below reads var(--...) and never a literal, so
 * a component cannot end up styled for one theme while sitting on the other's ground.
 *
 * The accent shifts between themes rather than staying put: #4bb3fd is legible on a
 * near-black panel but fails contrast on white, so light gets a darker blue. */
:root {
  --bg: #f6f8fb;
  --panel: #ffffff;
  --panel-2: #eef2f7;
  --line: #d5dee7;
  --line-soft: #e6ecf2;
  --fg: #0e1720;
  --muted: #5a6a7a;
  --accent: #0a6ab0;
  --on-accent: #ffffff;
  --ok: #1a7f37;
  --warn: #9a6700;
  --bad: #c1362c;
  --bad-wash: rgba(193, 54, 44, 0.08);
  --code-bg: #f2f5f9;
  --report-bg: #ffffff;
  --disabled: #8a96a3;
  /* JSON syntax colours, tuned per theme for contrast against --code-bg. */
  --j-key: #0a5fa8;
  --j-str: #0f7a55;
  --j-num: #a8530b;
  --j-lit: #7a3fb8;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* Only the tokens are redefined, never component rules. Guarded with :not([data-theme=
   "light"]) so an explicit light choice still beats a dark OS. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0d1117;
    --panel: #151b23;
    --panel-2: #1c232d;
    --line: #2a3441;
    --line-soft: #212936;
    --fg: #e6edf3;
    --muted: #93a1b1;
    --accent: #4bb3fd;
    --on-accent: #06121d;
    --ok: #3fb950;
    --warn: #d29922;
    --bad: #f85149;
    --bad-wash: rgba(248, 81, 73, 0.09);
    --code-bg: #0b0f14;
    --report-bg: #ffffff;
    --disabled: #6b7785;
    --j-key: #79c0ff;
    --j-str: #a5d6ff;
    --j-num: #ffa657;
    --j-lit: #d2a8ff;
  }
}

/* The explicit choice, so the toggle wins in the other direction too. */
:root[data-theme="dark"] {
  --bg: #0d1117;
  --panel: #151b23;
  --panel-2: #1c232d;
  --line: #2a3441;
  --line-soft: #212936;
  --fg: #e6edf3;
  --muted: #93a1b1;
  --accent: #4bb3fd;
  --on-accent: #06121d;
  --ok: #3fb950;
  --warn: #d29922;
  --bad: #f85149;
  --bad-wash: rgba(248, 81, 73, 0.09);
  --code-bg: #0b0f14;
  --report-bg: #ffffff;
  --disabled: #6b7785;
  --j-key: #79c0ff;
  --j-str: #a5d6ff;
  --j-num: #ffa657;
  --j-lit: #d2a8ff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

.topbar {
  display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap;
  padding: 1rem 1.5rem; border-bottom: 1px solid var(--line); background: var(--panel);
}
.brand-block { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; }
.brand { color: var(--fg); font-size: 1.25rem; font-weight: 650; text-decoration: none; }
.brand .dot { color: var(--accent); }
.byline { font-size: 10px; color: var(--muted); line-height: 1.2; }
.byline a { color: var(--muted); text-decoration: underline; }
.byline a:hover, .byline a:focus-visible { color: var(--fg); }
.tagline { color: var(--muted); font-size: 0.85rem; }

.account { margin-left: auto; display: flex; align-items: center; gap: 0.7rem; font-size: 0.85rem; }
.account-link { color: var(--accent); text-decoration: none; }
.account-link:hover { text-decoration: underline; }
.account-name { color: var(--muted); }
.account-form { display: inline; margin: 0; }
.link-button {
  background: none; border: 0; padding: 0; margin: 0; font: inherit; cursor: pointer;
  color: var(--accent); text-decoration: none;
}
.link-button:hover { text-decoration: underline; }

.job-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.job-row {
  display: flex; align-items: center; gap: 0.9rem; padding: 0.7rem 0.9rem;
  border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
  text-decoration: none; color: var(--fg);
}
.job-row:hover { border-color: var(--accent); }
.job-row .jobid { background: var(--panel-2); padding: 0.1rem 0.4rem; border-radius: 5px; }
.job-row .source { color: var(--muted); font-size: 0.85rem; }
.job-row .when { margin-left: auto; color: var(--muted); font-size: 0.8rem; }
.pagination { display: flex; gap: 1rem; margin-top: 1.1rem; }

.wrap { max-width: 1040px; margin: 0 auto; padding: 1.75rem 1.25rem 3rem; }
.foot { border-top: 1px solid var(--line); color: var(--muted); font-size: 0.8rem;
        padding: 1rem 1.5rem; }

h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
h2 { font-size: 1.1rem; margin: 0; }
.lede { color: var(--muted); margin-top: 0; }
.hint { color: var(--muted); font-size: 0.85rem; margin: 0.35rem 0 0.9rem; }
.hint.secure { color: var(--ok); }
.muted { color: var(--muted); }
.mono, code { font-family: var(--mono); font-size: 0.88em; }

.card, .panel {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 1.25rem; margin: 1.25rem 0;
}
fieldset { border: 1px solid var(--line); border-radius: 8px; margin: 0 0 1.1rem; padding: 0.9rem 1rem; }
legend { color: var(--muted); font-size: 0.8rem; letter-spacing: 0.04em; text-transform: uppercase; }

label { display: block; font-weight: 550; margin: 0.6rem 0 0.3rem; }
label.radio { display: flex; align-items: center; gap: 0.55rem; font-weight: 450; margin: 0.35rem 0; }
input[type="text"], input[type="url"], input[type="password"], input[type="file"], select {
  width: 100%; padding: 0.5rem 0.6rem; border-radius: 6px;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--fg);
  font: inherit;
}
input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
select option:disabled { color: var(--disabled); }

details.private { margin-top: 0.6rem; border: 1px dashed var(--line); border-radius: 8px; padding: 0.6rem 0.8rem; }
details summary { cursor: pointer; color: var(--accent); }

.actions { display: flex; align-items: center; gap: 0.9rem; }
button.primary, a.primary {
  background: var(--accent); color: var(--on-accent); border: 0; border-radius: 6px;
  padding: 0.6rem 1.1rem; font: inherit; font-weight: 650; cursor: pointer;
}
a.primary { display: inline-block; text-decoration: none; }
button.primary:hover, a.primary:hover { filter: brightness(1.08); }
button.primary:disabled, button.primary:disabled:hover {
  background: var(--disabled); color: var(--muted); cursor: not-allowed; filter: none;
}

/* htmx indicator styles are shipped here because the CSP forbids inline <style>. */
.htmx-indicator { display: none; color: var(--muted); }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: inline; }

/* Hide the source-type picker while a scan is being submitted. htmx puts .htmx-request on
   the form for the duration of the POST; on a validation error the class is removed and the
   picker returns, and on success the page redirects to the job. */
#scan-form.htmx-request .sources { display: none; }

.note { background: var(--panel-2); border-left: 3px solid var(--accent);
        padding: 0.6rem 0.8rem; border-radius: 0 6px 6px 0; margin: 0.8rem 0; }
.note.warn { border-left-color: var(--warn); }
.note.disclaimer { border-left-color: var(--warn); font-size: 0.92em; }
.errors { border: 1px solid var(--bad); background: var(--bad-wash);
          border-radius: 8px; padding: 0.7rem 0.9rem; margin-top: 0.9rem; }
.errors ul { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.error-detail { margin: 0; font-weight: 650; }
.error-detail .code { color: var(--muted); font-weight: 400; font-size: 0.85em; }

.meta { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; margin: 0.75rem 0 1.25rem; }
.meta dt { color: var(--muted); }
.meta dd { margin: 0; }
.jobid { background: var(--panel-2); padding: 0.1rem 0.4rem; border-radius: 5px; }

.status { border: 1px solid var(--line); border-radius: 10px; padding: 1rem 1.15rem; background: var(--panel); }
.status-line { display: flex; align-items: center; gap: 0.6rem; margin: 0 0 0.5rem; }
.badge { text-transform: uppercase; font-size: 0.72rem; letter-spacing: 0.06em;
         padding: 0.2rem 0.5rem; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); }
.badge-succeeded { color: var(--ok); border-color: var(--ok); }
.badge-failed, .badge-canceled, .badge-expired { color: var(--bad); border-color: var(--bad); }
.spinner { width: 0.7rem; height: 0.7rem; border-radius: 50%;
           border: 2px solid var(--line); border-top-color: var(--accent);
           display: inline-block; animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }

.scorecard { display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
             background: var(--panel-2); border-radius: 8px; padding: 0.9rem 1rem; margin: 0.9rem 0; }
.score-value { font-size: 2rem; font-weight: 700; }
.score-label { color: var(--muted); margin-left: 0.35rem; }
.risk { padding: 0.25rem 0.7rem; border-radius: 999px; border: 1px solid var(--line); }
.counts { list-style: none; display: flex; gap: 1rem; flex-wrap: wrap; margin: 0; padding: 0; }
.count span { font-weight: 700; }
.count-broken span { color: var(--bad); }
.count-weakened span { color: var(--warn); }
.count-ready span { color: var(--ok); }

.qramm { background: var(--panel-2); border-radius: 8px; padding: 0.9rem 1rem; margin: 0.9rem 0; }
.qramm h3 { margin: 0 0 0.2rem; font-size: 1rem; }
.qramm-score { margin: 0.4rem 0; }
.qramm-score span { font-size: 1.6rem; font-weight: 700; }
.qramm-practices { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.3rem; }
.qramm-practice { display: flex; gap: 0.7rem; align-items: baseline; }
.qramm-practice-id { color: var(--accent); font-family: var(--mono); font-size: 0.82rem; }
.qramm-practice-label { flex: 1; }
.qramm-level { font-weight: 700; }
.qramm-level-1, .qramm-level-2 { color: var(--bad); }
.qramm-level-3 { color: var(--warn); }
.qramm-level-4, .qramm-level-5 { color: var(--ok); }

/* The dependency card borrows the QRAMM card's frame: it sits in the same column, is
   read at the same moment, and a second visual language would imply a second kind of
   claim. Its counts reuse .counts so "3 reachable" lines up with "12 assets" above. */
.deps { background: var(--panel-2); border-radius: 8px; padding: 0.9rem 1rem; margin: 0.9rem 0; }
.deps h3 { margin: 0 0 0.2rem; font-size: 1rem; }
.deps .counts { margin-top: 0.6rem; }

.events { margin-top: 0.75rem; }
.event-list { list-style: none; margin: 0.5rem 0 0; padding: 0; font-family: var(--mono); font-size: 0.82rem; }
.event { display: flex; gap: 0.7rem; padding: 0.15rem 0; }
.event-code { color: var(--accent); }
.event-error .event-msg { color: var(--bad); }
.event-warn .event-msg { color: var(--warn); }

.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.fmt { color: var(--muted); font-size: 0.8rem; }
.download, a { color: var(--accent); }
.downloads { font-size: 0.9rem; }

pre.json, pre.cbom {
  background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px;
  padding: 0.9rem; overflow: auto; max-height: 640px;
  font-family: var(--mono); font-size: 0.82rem; line-height: 1.45;
}
.jk { color: var(--j-key); }
.js { color: var(--j-str); }
.jn { color: var(--j-num); }
.jl { color: var(--j-lit); }

.report-frame, .report-object { border: 1px solid var(--line); border-radius: 8px;
                                background: var(--report-bg); margin-top: 0.9rem; }

/* --- landing-page activity board -------------------------------------------
   Three rolling lists of what was scanned recently. Each row is a subject and a
   score; there is deliberately no job id and no link, so a row cannot be turned
   into somebody else's job. Collapses to one column on narrow screens rather
   than letting long image references force the page to scroll sideways. */
.board {
  display: grid; gap: 1rem; margin-top: 2.25rem;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.board-col {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; padding: 0.9rem 1rem 1rem;
}
.board-title {
  margin: 0 0 0.6rem; font-size: 0.8rem; font-weight: 650;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
}
.board-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.board-row {
  display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.35rem 0; border-top: 1px solid var(--line-soft, var(--line));
}
.board-row:first-child { border-top: 0; }
/* The subject is the only elastic part: truncate it rather than let a long image
   reference push the score out of the card. */
.board-subject {
  flex: 1 1 auto; min-width: 0; font-family: var(--mono); font-size: 0.82rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg);
}
.board-score {
  flex: 0 0 auto; font-variant-numeric: tabular-nums; font-weight: 650;
  font-size: 0.85rem; min-width: 2.2ch; text-align: right;
}
.score-good { color: var(--ok); }
.score-mid { color: var(--warn); }
.score-bad { color: var(--bad); }
.score-unknown { color: var(--muted); }
.board-empty { margin: 0; color: var(--muted); font-size: 0.85rem; }

/* Sits directly under the submit button, so the choice is visible at the moment of
   submitting rather than buried up in the form. */
.privacy-opt-out { margin-top: 0.55rem; color: var(--muted); font-size: 0.85rem; }

/* Inside the source fieldset rather than under the button: unlike the privacy opt-out
   this changes what the scan DOES, so it belongs with the other scan inputs. Its hint
   uses the same muted style the fieldset's other hints do. */
.cryptodeps-opt-in { margin-top: 0.75rem; font-size: 0.9rem; }

/* --- theme toggle ----------------------------------------------------------
   Sits with the account controls. Shows the theme it will switch TO, which is
   the thing a reader wants to know before clicking. */
/* Hidden until theme.js marks the document ready: the control is useless without
   scripting, and CSP forbids the <noscript><style> trick. */
/* Specificity matters here: .topbar-button below also sets display, so the hidden
   state has to outweigh it rather than merely precede it. */
html:not(.js-ready) .theme-toggle { display: none; }
/* One style for every pill in the topbar - the theme switch and the legal links -
   so they read as one row of controls rather than a button next to two links. */
.topbar-button {
  background: none; border: 1px solid var(--line); border-radius: 6px;
  padding: 0.2rem 0.55rem; font: inherit; font-size: 0.8rem; cursor: pointer;
  color: var(--muted); line-height: 1.6; text-decoration: none;
  display: inline-block; white-space: nowrap;
}
.topbar-button:hover, .topbar-button:focus-visible {
  color: var(--fg); border-color: var(--accent); text-decoration: none;
}
/* An icon-only pill. The glyph stays INLINE - `display: block` would leave the pill
   with no text baseline, and the topbar aligns its items on the baseline, so the box
   hung about 10px above its lettered neighbours. Inline, the line box's strut sets the
   baseline exactly as it does in the other three pills, and the shared line-height
   gives all four the same height. */
.topbar-icon { padding-left: 0.45rem; padding-right: 0.45rem; }
.topbar-icon svg { width: 1em; height: 1em; vertical-align: middle; }

/* --- session block (enterprise) --------------------------------------------
   Who is signed in, and the way out. Borrows the theme toggle's styling so the
   two controls read as one group rather than as a button someone bolted on.
   Unlike that toggle it is NOT hidden without scripting: signing out is a form
   POST and works with JavaScript off, and a sign-out you cannot reach is worse
   than one that is plainly styled. */
.session-block { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.session-user {
  color: var(--muted); font-size: 0.8rem; max-width: 16ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.logout-form { margin: 0; }
.logout-button {
  background: none; border: 1px solid var(--line); border-radius: 6px;
  padding: 0.2rem 0.55rem; font: inherit; font-size: 0.8rem; cursor: pointer;
  color: var(--muted); line-height: 1.6;
}
.logout-button:hover, .logout-button:focus-visible {
  color: var(--fg); border-color: var(--accent);
}

/* --- PQC explainer ---------------------------------------------------------
   Shown on the landing page, the job list and a job's page. Sits below the
   page's own content: someone who came to run a scan reaches the form first,
   and someone who came for a result reaches the result first. Constrained to a
   readable measure rather than running the full page width. */
.pqc-info {
  margin-top: 2.5rem; padding-top: 1.5rem;
  border-top: 1px solid var(--line);
}
/* The readable-line-length cap used to live on the section, which silently defeated the
   two-column grid inside it: 68ch is narrower than two columns need, so auto-fit only
   ever produced one. The cap now belongs to each column instead, where it does the same
   job for the same reason. */
.pqc-info > p, .pqc-info > h2 { max-width: 68ch; }
.pqc-info h2 { font-size: 1.05rem; margin: 0 0 0.5rem; }
.pqc-info h3 { font-size: 0.9rem; margin: 1.1rem 0 0.3rem; color: var(--fg); }
.pqc-info p { margin: 0 0 0.6rem; color: var(--muted); font-size: 0.9rem; line-height: 1.6; }
/* Matched on two selectors so it outranks `.pqc-info p` on specificity rather
   than by !important, which would just move the fight somewhere else. */
.pqc-info .pqc-contact { margin-top: 1.1rem; font-size: 0.9rem; }
/* --- PQC intro -------------------------------------------------------------
   The same two-column grid as the explainer, but ABOVE the page's own content
   rather than below it: these two paragraphs are why someone would run a scan,
   so they lead. Its own rules rather than a shared selector list with
   `.pqc-info` - this block is read first and is sized against the h1 beneath it,
   not against a card heading buried in a long section. */
.pqc-intro { margin: 0 0 2.25rem; padding-bottom: 1.4rem; border-bottom: 1px solid var(--line); }
/* On /jobs and a job page it trails the page's own content instead of leading it. */
.pqc-intro:not(:first-child) { margin-top: 2.5rem; }
/* ...and there the explainer follows immediately, whose own top rule already does the
   separating. Two rules a few rem apart read as a mistake. */
.pqc-intro:has(+ .pqc-info) { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }
.pqc-intro h2 { font-size: 1rem; margin: 0 0 0.35rem; color: var(--fg); }
.pqc-intro p { margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.6; }
.pqc-intro .info-grid { margin-top: 0; }

/* Exactly two columns, stated rather than inferred. auto-fit + minmax was tried first and
   is the wrong tool here: it means "as many as fit", and once the section's width cap was
   removed three 19rem columns fit the 1040px container. minmax(0, 1fr) rather than 1fr so a
   long <code> span cannot push a column wider than its share. */
.info-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.4rem 2rem; margin-top: 0.6rem;
}
/* One column once a half-width column stops being readable prose. Keyed to content width,
   not a device: these paragraphs are dense. */
@media (max-width: 46rem) {
  .info-grid { grid-template-columns: minmax(0, 1fr); }
}
/* The first heading in each card must not inherit the 1.1rem top margin that separates
   consecutive headings in a single column - in a grid it just pushes the row apart. */
.info-grid .info-card h3 { margin-top: 0.9rem; }
/* Each column is a container, not a flow position: the source-scan sections belong
   together on the right, and auto-flow would start them halfway down the left column. */
.info-col { display: flex; flex-direction: column; }
/* The second section's heading. Matches the first section's h2 so the two read as peers,
   with more room above it than a card heading gets - it separates two groups rather than
   introducing one paragraph. */
.info-section-heading { margin-top: 2rem; }
.info-card code {
  font-size: 0.85em; padding: 0.05rem 0.25rem; border-radius: 3px;
  background: var(--panel-2); color: var(--fg);
}

/* Footer link to the privacy notice. */
.foot a { color: var(--muted); margin-left: 1rem; }
.foot a:hover { color: var(--fg); }

/* Consent modal. A bottom strip reads as page furniture and gets ignored, and
   acknowledging the notice is a precondition of scanning, so it is presented as a
   decision to make: a dimmed backdrop over the whole viewport with a centred dialog.
   The page behind is deliberately not interactive until the visitor answers. */
.consent-overlay {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center; padding: 1.5rem;
  background: rgb(0 0 0 / 55%);
}
.consent-overlay[hidden] { display: none; }
/* Locks the page behind the modal. Set from consent.js as a class, because the CSP
   forbids inline styles. */
html.consent-open, html.consent-open body { overflow: hidden; }
.consent-dialog {
  width: 100%; max-width: 38rem; max-height: 85vh; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 18px 50px rgb(0 0 0 / 35%); padding: 1.5rem;
}
.consent-lead { margin: 0 0 0.6rem; font-size: 1.1rem; font-weight: 650; color: var(--fg); }
.consent-text p { margin: 0 0 0.7rem; font-size: 0.88rem; color: var(--muted); line-height: 1.55; }
.consent-optin {
  display: flex; align-items: flex-start; gap: 0.5rem; margin: 0.9rem 0 1.2rem;
  font-size: 0.82rem; color: var(--muted); cursor: pointer;
}
.consent-optin input { margin-top: 0.2rem; flex: none; }
.consent-actions { display: flex; gap: 0.7rem; flex-wrap: wrap; align-items: center; }
button.secondary {
  background: transparent; color: var(--fg); border: 1px solid var(--line);
  border-radius: 6px; padding: 0.6rem 1.1rem; font: inherit; cursor: pointer;
}
button.secondary:hover { border-color: var(--accent); }

/* Long-form copy: the privacy notice is the only page of running prose in the app. */
.prose h1 { font-size: 1.5rem; margin: 0 0 0.2rem; }
.prose h2 { font-size: 1.05rem; margin: 1.5rem 0 0.4rem; }
.prose p, .prose li { line-height: 1.6; }
.prose ul { padding-left: 1.2rem; }
.prose li { margin: 0.3rem 0; }
.prose table {
  border-collapse: collapse; width: 100%; margin: 0.7rem 0 1rem; font-size: 0.85rem;
}
.prose th, .prose td {
  border: 1px solid var(--line); padding: 0.45rem 0.6rem; text-align: left; vertical-align: top;
}
.prose th { background: var(--panel-2); }
