/* OMIWORLD portal — design system.

   Every colour here resolves through a --brand-* hook that portal/base.html
   writes from the client config. Before this block existed the stylesheet
   defined its own palette and ignored all six brand variables, so a client's
   configured identity reached no pixel: the live portal rendered this file's
   hardcoded brown while the newsletter rendered Oblate maroon. The literal
   values after each var() are the fallback for the one case base.html cannot
   cover, portal.css served without its <style> block.

   Tokens that are not brand hooks are mixed from the hooks, so one client
   config repaints the whole portal. The mix percentages are measured against
   WCAG 2.1 AA, not chosen by eye. tests/unit/test_portal_design_tokens.py
   holds the check. */

:root {
    /* Brand hooks */
    --color-primary: var(--brand-primary, #6b4f3a);
    --color-secondary: var(--brand-secondary, #a07d5f);
    --color-accent: var(--brand-accent, #a07d5f);
    --color-page: var(--brand-bg, #f8f7f4);
    --color-text: var(--brand-text, #2a2a2a);

    /* Surfaces. A card is white on a tinted page, which is how the newsletter
       separates content from ground. */
    --color-surface: #fff;
    --color-surface-alt: #faf7f2;
    --color-surface-alt: color-mix(in srgb, var(--color-page) 55%, #fff);
    --color-sunken: #ece5d8;
    --color-sunken: color-mix(in srgb, var(--color-primary) 8%, var(--color-page));
    --color-on-primary: #fff;

    /* Muted ink. 80% is the lightest mix that clears 4.5:1 on all four
       surfaces above, for the neutral palette and for a warm parchment one.
       75% shipped first and read 4.42:1 on --color-sunken, under the AA floor.
       tests/unit/test_portal_accessibility.py measures every pair.
       ponytail: a client whose own text and background sit near the AA floor
       needs a darker mix; measure before shipping such a palette. */
    --color-muted: #6b6b6b;
    --color-muted: color-mix(in srgb, var(--color-text) 80%, var(--color-page));

    /* Lines. The hairline carries the accent, which is why the accent may be a
       colour too pale to set text in. */
    --color-border: #e0ddd5;
    --color-border: color-mix(in srgb, var(--color-accent) 38%, var(--color-page));
    /* The edge of a form control, not a decorative rule. WCAG 2.1 SC 1.4.11
       asks 3:1 for the visual boundary of a control, and 15% gave 1.37:1 on
       white, so every input on the portal had a border a low-vision reader
       could not find. 60% clears 3:1 on white and on the page tint. */
    --color-border-strong: #8b8985;
    --color-border-strong: color-mix(in srgb, var(--color-text) 60%, var(--color-page));
    --color-rule: var(--color-accent);

    /* Status. Fixed, because a warning must not follow the brand. */
    --color-error: #b3261e;
    --color-error-bg: #fdecea;
    --color-error-border: #f5c6cb;
    --color-success: #2e7d32;
    --color-success-bg: #e8f5e9;
    --color-success-border: #a5d6a7;
    --color-notice: #856404;
    --color-notice-bg: #fff3cd;

    /* Type. --brand-font drives running prose, so the portal reads like the
       newsletter. Controls and labels stay on the system sans stack, which is
       also what the newsletter does with its captions and eyebrows.
       Write font stacks without quotes: base.html injects the value into a
       <style> block through Jinja's HTML escaping, and an escaped quote is not
       a quote to the CSS parser. */
    --font-body: var(--brand-font, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif);
    --font-display: var(--brand-font, Georgia, Times New Roman, Iowan Old Style, serif);
    --font-ui: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;

    --text-xs: 0.8125rem;
    --text-sm: 0.9375rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.375rem;
    --text-2xl: 1.75rem;
    --text-3xl: clamp(2rem, 6vw, 3rem);

    --leading-tight: 1.2;
    --leading-heading: 1.3;
    --leading-body: 1.65;
    --leading-prose: 1.75;

    --tracking-wide: 0.08em;
    --tracking-label: 0.12em;

    --weight-medium: 600;
    --weight-bold: 700;

    /* Space, on a 4px base */
    --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;
    --space-8: 4rem;

    /* Shape */
    --radius-sm: 3px;
    --radius: 6px;
    --radius-lg: 10px;
    --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 14px rgba(0, 0, 0, 0.05);
    --shadow-raised: 0 2px 6px rgba(0, 0, 0, 0.07), 0 12px 28px rgba(0, 0, 0, 0.08);

    /* Layout */
    --width-page: 1080px;
    --width-content: 720px;
    --width-form: 440px;

    /* Controls. 2.75rem is 44px, the smallest comfortable touch target. */
    --control-height: 2.75rem;
    --btn-padding: 0.7rem 1.5rem;
    --field-padding: 0.65rem 0.75rem;
    --card-padding: var(--space-5);
    --focus-color: var(--color-secondary);
    --focus-width: 3px;
    --focus-offset: 2px;

    /* Aliases for rules not yet migrated to the tokens above. */
    --bg: var(--color-page);
    --fg: var(--color-text);
    --accent: var(--color-primary);
    --accent-light: var(--color-secondary);
    --border: var(--color-border);
    --error: var(--color-error);
    --success: var(--color-success);
    --muted: var(--color-muted);
    --max-width: var(--width-content);
}

* { box-sizing: border-box; }

/* The column keeps the footer at the foot of a short page. The archive with
   two issues in it ended a third of the way down and left the footer stranded
   mid screen with page colour below it. */
body {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    font-family: var(--font-body);
    font-size: var(--text-base);
    background: var(--color-page);
    color: var(--color-text);
    line-height: var(--leading-body);
    -webkit-text-size-adjust: 100%;
}
/* width:100% is not optional here. As a flex item the main column is sized to
   fit its content instead of the viewport, and the translate page's widest
   control pushed the document to 414px on a 360px screen. */
body > .container { flex: 1 0 auto; width: 100%; }

/* Headings carry the display serif, which is what the newsletter sets its own
   headings in. Without this they inherited the body leading and a two line
   heading opened a gap on a phone. */
h1, h2, h3, h4 {
    margin: 0 0 var(--space-3);
    font-family: var(--font-display);
    line-height: var(--leading-heading);
    color: var(--color-primary);
    text-wrap: balance;
}
h1 { font-size: var(--text-2xl); }
h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); }
h4 { font-size: var(--text-base); }

/* Prose links had no rule at all, so an issue body rendered browser blue on
   parchment. */
a { color: var(--color-secondary); }

/* One visible ring on every interactive element. :where() keeps specificity at
   zero so a component can restyle the control without losing the ring. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: var(--focus-width) solid var(--focus-color);
    outline-offset: var(--focus-offset);
    border-radius: var(--radius-sm);
}

/* The skip link is off screen until it takes focus. */
.skip-link {
    position: absolute;
    left: var(--space-2);
    top: -4rem;
    z-index: 10;
    padding: var(--space-2) var(--space-4);
    background: var(--color-primary);
    color: var(--color-on-primary);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    border-radius: var(--radius);
    text-decoration: none;
    transition: top 0.15s ease;
}
.skip-link:focus { top: var(--space-2); }

/* Text a screen reader must read and a screen must not show.
   left: 0 is not decoration. Without it the box keeps its static position,
   and inside a table that scrolls sideways that position is off the right of
   the page: the header cell of the translate jobs table put it at x=462 in a
   375px viewport and the whole document scrolled 88px sideways, masthead and
   all. Pinning left to the containing block keeps it clipped where it sits. */
.visually-hidden {
    position: absolute;
    left: 0;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* An eyebrow names the section above the heading, in the UI sans, the way the
   newsletter labels its own sections. */
.eyebrow {
    margin: 0 0 var(--space-2);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--color-muted);
}
.lead {
    max-width: 38rem;
    margin: 0 0 var(--space-6);
    color: var(--color-muted);
    font-size: var(--text-lg);
}
.page-head { margin-bottom: var(--space-6); }
.page-head h1 { margin-bottom: var(--space-2); }

/* Header. The maroon band is the newsletter masthead's own rule, so a reader
   who arrives from the email meets the same object. */
.site-header {
    background: var(--color-surface);
    border-top: 4px solid var(--color-primary);
    border-bottom: 1px solid var(--color-border);
}
.nav {
    max-width: var(--width-page);
    margin: 0 auto;
    padding: var(--space-3) var(--space-5);
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-5);
}
.nav .brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-display);
    font-weight: var(--weight-bold);
    font-size: var(--text-xl);
    text-decoration: none;
    color: var(--color-primary);
    letter-spacing: var(--tracking-wide);
}
/* A configured logo is whatever size the client uploaded. Unconstrained, a
   600x400 file gave a 476px header and scrolled a phone sideways. */
.brand-logo { height: 2rem; width: auto; vertical-align: middle; }
/* gap, not margin-left: the last child is the log-out form, which a margin on
   the anchors never reached, so "Subscribe", the user name and "Log out"
   collided into one word at every width. */
.nav-links {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0 var(--space-5);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
}
.nav-links a {
    display: inline-flex;
    align-items: center;
    min-height: var(--control-height);
    text-decoration: none;
    color: var(--color-text);
    border-bottom: 2px solid transparent;
}
.nav-links a:hover {
    color: var(--color-primary);
    border-bottom-color: var(--color-accent);
}

/* Layout */
.container {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: var(--space-6) var(--space-5) var(--space-8);
}
/* The landing page opts into the wide measure. Reading pages keep the narrow
   one, because a 1080px line of prose is unreadable. */
.container-wide { max-width: var(--width-page); }

.site-footer {
    text-align: center;
    padding: var(--space-6) var(--space-5);
    background: var(--color-surface-alt);
    color: var(--color-muted);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    border-top: 1px solid var(--color-border);
}
.site-footer p { max-width: var(--width-content); margin: 0 auto; }

/* Hero */
.hero {
    text-align: center;
    max-width: 44rem;
    margin: 0 auto;
    padding: var(--space-7) 0 var(--space-6);
}
.hero h1 {
    font-size: var(--text-3xl);
    margin: 0 0 var(--space-4);
    letter-spacing: 0.04em;
}
.hero .subtitle {
    font-size: var(--text-lg);
    color: var(--color-muted);
    max-width: 34rem;
    margin: 0 auto var(--space-6);
}
.cta { display: flex; gap: var(--space-4); justify-content: center; flex-wrap: wrap; }

/* The rule the newsletter draws between its sections, drawn again here so the
   portal and the email carry the same ornament. It is CSS, not the U+2766
   fleuron the email uses: no serif on Linux carries that glyph, so the browser
   substitutes a colour emoji and puts a pink heart on a parchment page.
   Decoration only, and the markup hides it from assistive technology. */
.ornament {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin: var(--space-7) auto 0;
    max-width: 22rem;
}
.ornament::before,
.ornament::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--color-border);
}
.ornament > span {
    width: 0.5rem;
    height: 0.5rem;
    background: var(--color-accent);
    transform: rotate(45deg);
}

/* Buttons. min-height, not padding alone, guarantees the 44px touch target
   whatever the label wraps to. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--control-height);
    padding: var(--btn-padding);
    background: var(--color-primary);
    color: var(--color-on-primary);
    border: 1px solid var(--color-primary);
    border-radius: var(--radius);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    line-height: var(--leading-tight);
    text-decoration: none;
    text-align: center;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.btn:hover { background: var(--color-secondary); border-color: var(--color-secondary); }
.btn-secondary {
    background: var(--color-surface);
    color: var(--color-primary);
    border-color: var(--color-primary);
}
.btn-secondary:hover { background: var(--color-primary); color: var(--color-on-primary); }
.btn-danger { background: var(--color-error); border-color: var(--color-error); }
.btn-danger:hover { background: #8a1d17; border-color: #8a1d17; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

/* Features grid. 15rem, not 200px, so the columns track the reader's own font
   size instead of collapsing to three thin cards when they enlarge it. */
.features {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-5);
    margin-top: var(--space-7);
}
/* Cards. .feature and .panel are the same object with different content. */
.feature, .panel {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--card-padding);
    box-shadow: var(--shadow-card);
}
/* The card is one target: the link stretches over it, so a thumb anywhere on
   the card opens it, and the anchor stays the only tab stop. */
.feature {
    position: relative;
    display: flex;
    flex-direction: column;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.feature:hover {
    border-color: var(--color-accent);
    box-shadow: var(--shadow-raised);
    transform: translateY(-2px);
}
.feature h2 {
    margin: 0 0 var(--space-2);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    line-height: var(--leading-heading);
    color: var(--color-primary);
}
.feature p { color: var(--color-muted); margin: 0 0 var(--space-4); font-size: var(--text-sm); flex: 1; }
.feature a {
    color: var(--color-secondary);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    text-decoration: none;
}
.feature a::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.feature a:hover { text-decoration: underline; }

@media (prefers-reduced-motion: reduce) {
    .feature { transition: none; }
    .feature:hover { transform: none; }
}

/* Forms */
.form-section { max-width: var(--width-form); margin: 0 auto; }
.field { margin-bottom: var(--space-4); }
.field label {
    display: block;
    margin-bottom: var(--space-1);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--color-text);
}
/* font-size stays at 1rem: anything smaller makes iOS Safari zoom the page on
   focus, which strands a phone user mid form. */
.field input, .field select, .field textarea {
    width: 100%;
    min-height: var(--control-height);
    padding: var(--field-padding);
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius);
    font-size: var(--text-base);
    font-family: var(--font-ui);
}
.field input:hover, .field select:hover, .field textarea:hover {
    border-color: var(--color-primary);
}
.help {
    margin: var(--space-1) 0 0;
    color: var(--color-muted);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
}
/* A placeholder is a hint, not a label, so it may be quieter than body text.
   The browser default is an unmeasured grey; --color-muted is measured. */
::placeholder { color: var(--color-muted); opacity: 1; }
/* A field the reader may read but not change. It shipped with an inline
   background-color: #f0f0f0, which is a grey no client config can repaint. */
.field input:disabled, .field select:disabled, .field textarea:disabled,
.field input[readonly], .field textarea[readonly] {
    background: var(--color-sunken);
    color: var(--color-muted);
    cursor: not-allowed;
}
/* A row of buttons under a form. Every page that had one wrote its own
   inline flex with a hardcoded 12px gap, and none of them stacked on a
   phone, so two side-by-side buttons overflowed a 360px screen. */
.actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-5);
}

/* Alerts */
.alert {
    padding: 0.875rem 1rem;
    border-radius: var(--radius);
    margin-bottom: 1rem;
}
.alert-success {
    background: var(--color-success-bg);
    border: 1px solid var(--color-success-border);
    color: var(--color-success);
}
.alert-error {
    background: var(--color-error-bg);
    border: 1px solid var(--color-error-border);
    color: var(--color-error);
}
/* Every alert on the portal used to carry inline paragraph margins, because
   the default 1em top margin opened a gap above the first line. */
.alert p { margin: 0 0 var(--space-2); }
.alert p:last-child { margin-bottom: 0; }
.alert h2 { margin: 0 0 var(--space-2); font-size: var(--text-lg); color: inherit; }

/* Newsletter list. Every issue carries the same title, "OMIWORLD Weekly
   Digest", so the date and the subject are what a reader actually picks from.
   They lead the card; the title follows. */
.issue-list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-4); }
.issue {
    position: relative;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-left: 3px solid var(--color-accent);
    border-radius: var(--radius-lg);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.issue:hover { border-color: var(--color-secondary); box-shadow: var(--shadow-card); }
.issue > a {
    display: block;
    padding: var(--space-5);
    text-decoration: none;
    color: inherit;
}
.issue .date {
    display: block;
    margin: 0 0 var(--space-2);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--color-muted);
}
/* h3 since the template group above it owns the h2: the archive is now
   grouped, and skipping a heading level breaks the page for a screen reader.
   Both are matched so an issue card looks the same either way. */
.issue h2, .issue h3 {
    margin: 0 0 var(--space-2);
    font-size: var(--text-lg);
    color: var(--color-primary);
}

/* One template's run of the back catalogue. */
.group-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin: var(--space-6) 0 var(--space-3);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--color-border);
    color: var(--color-primary);
    font-size: var(--text-xl);
    scroll-margin-top: var(--space-6);
}
.group-count {
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--color-muted);
}
.issue-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin-bottom: var(--space-4);
}
.issue-nav a {
    color: var(--color-secondary);
    font-size: var(--text-sm);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}
.issue-nav a:hover, .issue-nav a:focus-visible { border-bottom-color: currentColor; }
.issue .meta { margin: 0; color: var(--color-text); font-size: var(--text-base); }
.issue .more {
    display: inline-block;
    margin-top: var(--space-3);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--color-secondary);
}
.issue:hover .more { text-decoration: underline; }
.empty { color: var(--color-muted); }
.empty-state { text-align: center; }
.empty-state p { margin: 0 0 var(--space-4); }

@media (prefers-reduced-motion: reduce) {
    .issue { transition: none; }
}

/* Newsletter view */
.newsletter-view .issue-header {
    border-bottom: 2px solid var(--color-rule);
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-6);
}
.newsletter-view .issue-header h1 { margin: 0 0 var(--space-2); font-size: var(--text-2xl); }
.newsletter-view .subject { margin: 0; color: var(--color-text); font-size: var(--text-lg); }
.newsletter-view .date { margin: var(--space-3) 0 0; font-family: var(--font-ui); font-size: var(--text-sm); color: var(--color-muted); }
/* The body is the newsletter's own HTML, styled inline for an email client.
   Those inline rules win, so this block only stops the wide objects in it from
   scrolling a phone sideways. */
.issue-body { line-height: var(--leading-prose); overflow-wrap: break-word; }
.issue-body h2, .issue-body h3 { color: var(--color-primary); }
.issue-body a { color: var(--color-secondary); }
.issue-body img { max-width: 100%; height: auto; }
.issue-body table { max-width: 100%; table-layout: fixed; }
.issue-foot {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-7);
    padding-top: var(--space-5);
    border-top: 1px solid var(--color-border);
}

/* Chat */
.chat-section { max-width: var(--width-content); margin: 0 auto; }
.hint { color: var(--color-muted); }
.chat-window {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
    box-shadow: var(--shadow-card);
    overflow: hidden;
    margin-top: var(--space-5);
}
/* vh, not a fixed 380px: on a 360px phone in landscape a fixed transcript ate
   the whole viewport and pushed the composer off screen. */
.messages {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: clamp(20rem, 52vh, 30rem);
    overflow-y: auto;
    padding: var(--space-4);
}
/* The transcript opened as a blank white rectangle with no hint of what to
   ask. The JS removes this the moment the first message arrives. */
.chat-empty {
    margin: auto;
    max-width: 28rem;
    text-align: center;
    color: var(--color-muted);
    font-size: var(--text-sm);
}
.message {
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    max-width: 85%;
    font-size: var(--text-sm);
    line-height: var(--leading-body);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.message.user {
    background: var(--color-primary);
    color: var(--color-on-primary);
    margin-left: auto;
    border-bottom-right-radius: var(--radius-sm);
}
.message.assistant {
    background: var(--color-sunken);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-bottom-left-radius: var(--radius-sm);
}
.message.system {
    background: var(--color-notice-bg);
    color: var(--color-notice);
    font-style: italic;
    margin: 0 auto;
    max-width: 100%;
    text-align: center;
}
.chat-form {
    display: flex;
    border-top: 1px solid var(--color-border);
    background: var(--color-surface);
}
/* min-width:0 is what keeps the composer inside a 360px screen: a flex item
   floors at its content width otherwise, and the placeholder pushed the Send
   button off the edge. */
.chat-form input {
    flex: 1;
    min-width: 0;
    min-height: var(--control-height);
    border: none;
    padding: var(--space-3) var(--space-4);
    font-size: var(--text-base);
    font-family: var(--font-ui);
    color: var(--color-text);
    background: var(--color-surface);
}
/* The window clips its overflow, so an outset ring loses its bottom edge
   behind the frame. Inset it here only. The ring itself still comes from the
   global :focus-visible rule. */
.chat-form input:focus-visible { outline-offset: calc(-1 * var(--focus-width)); }
.chat-form button {
    flex: none;
    min-height: var(--control-height);
    border: none;
    background: var(--color-primary);
    color: var(--color-on-primary);
    padding: 0 var(--space-5);
    cursor: pointer;
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
}
.chat-form button:hover { background: var(--color-secondary); }
.chat-form button:disabled { opacity: 0.5; cursor: not-allowed; }

/* ---- Auth, translate, preferences, support ---- */

.nav-logout {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
}
.nav-user {
    color: var(--color-muted);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
}
/* A button that reads as a link. min-height keeps it on the 44px target the
   nav anchors already meet, so "Log out" is not the one control on the
   masthead a thumb misses. */
.linklike {
    display: inline-flex;
    align-items: center;
    min-height: var(--control-height);
    background: none;
    border: none;
    padding: 0;
    color: var(--color-secondary);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    cursor: pointer;
    text-decoration: underline;
}
.linklike:hover { color: var(--color-primary); }

.panel { margin-bottom: var(--space-5); }
.panel.narrow { max-width: var(--width-form); }
.panel h1, .panel h2 { margin-top: 0; }

/* .form stacks label above control. It predates .field and is what the login
   and translate pages use; the label styling is the same on both so a reader
   meets one form, not two. */
.form { display: flex; flex-direction: column; gap: var(--space-1); }
.form label {
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--color-text);
}
.form input[type="text"],
.form input[type="email"],
.form input[type="password"],
.form input[type="file"],
.form select {
    min-height: var(--control-height);
    padding: var(--field-padding);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius);
    font-family: var(--font-ui);
    font-size: var(--text-base);
    color: var(--color-text);
    background: var(--color-surface);
    margin-bottom: var(--space-3);
}
.form input[type="file"]:hover,
.form input[type="text"]:hover,
.form input[type="email"]:hover,
.form input[type="password"]:hover,
.form select:hover { border-color: var(--color-primary); }
.form button[type="submit"] {
    align-self: flex-start;
    min-height: var(--control-height);
    background: var(--color-primary);
    color: var(--color-on-primary);
    border: 1px solid var(--color-primary);
    border-radius: var(--radius);
    padding: var(--btn-padding);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    cursor: pointer;
}
.form button[type="submit"]:hover {
    background: var(--color-secondary);
    border-color: var(--color-secondary);
}
.form button[type="submit"]:disabled { opacity: 0.5; cursor: not-allowed; }

.error {
    color: var(--color-error);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    margin: var(--space-2) 0;
}

/* A group of checkboxes. The label wraps its own control, which is why none
   of them needs a for= attribute. */
.langs {
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius);
    padding: var(--space-3) var(--space-4);
    margin: 0 0 var(--space-4);
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-5);
}
.langs legend {
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--color-text);
    padding: 0 var(--space-1);
}
.langs label {
    display: inline-flex;
    align-items: center;
    min-height: var(--control-height);
    gap: var(--space-2);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--color-text);
}

/* A table of rows a reader scans. Six columns do not fit 360px. A bare table
   does not overflow, it compresses: measured at 375px, the title column came
   out about 80px wide and "How often does the newsletter go out" wrapped to
   one word a line. The wrapper gives the table its natural width and scrolls
   it, and it is focusable so a keyboard reaches the columns on the right. */
.table-scroll {
    overflow-x: auto;
    margin: 0 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
}
.data-table {
    width: 100%;
    min-width: 32rem;
    border-collapse: collapse;
    font-family: var(--font-ui);
    font-size: var(--text-sm);
}
.data-table th, .data-table td {
    text-align: left;
    padding: var(--space-3);
    border-bottom: 1px solid var(--color-border);
    vertical-align: top;
}
.data-table tr:last-child td { border-bottom: none; }
/* Short columns take their content width, so the one column of prose keeps
   what is left. Without this, "How often does the newsletter go out" wrapped
   to one word a line inside an 80px column on a phone. */
.data-table td { white-space: nowrap; }
.data-table td.wrap { white-space: normal; min-width: 14rem; }
.data-table th {
    color: var(--color-muted);
    font-weight: var(--weight-medium);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    white-space: nowrap;
}

/* The language picker on the page a newsletter link lands on. It carried the
   client palette as inline hex in the template, which put OMIWORLD colours
   inside semkit; the pills read the tokens now. */
.lang-picker { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; }
.lang-pill {
    display: inline-flex;
    align-items: center;
    min-height: var(--control-height);
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-primary);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    text-decoration: none;
}
.lang-pill:hover { border-color: var(--color-primary); }
/* The chosen language is not marked by colour alone: the markup carries
   aria-current, which is what a screen reader reads. */
.lang-pill[aria-current] {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: var(--color-on-primary);
}
.lang-pill[aria-current]:hover { background: var(--color-secondary); border-color: var(--color-secondary); }

/* The progress line, "Searching the archive". It shipped at opacity 0.6, which
   composites #856404 on #fff3cd down to about 2.6:1 and fails AA. Italic
   carries the difference instead. */
.message.status {
    font-style: italic;
}

/* Phones. Below 600px the masthead stacks, because the brand and five links
   never fit one 360px row and wrapped into a ragged block. */
@media (max-width: 600px) {
    .nav {
        flex-direction: column;
        align-items: flex-start;
        gap: 0;
        padding: var(--space-3) var(--space-4);
    }
    .nav-links { gap: 0 var(--space-4); }
    .container { padding: var(--space-5) var(--space-4) var(--space-7); }
    .hero { padding: var(--space-6) 0 var(--space-5); }
    .issue > a { padding: var(--space-4); }
    .cta .btn, .issue-foot .btn, .actions .btn { width: 100%; }
    .actions { gap: var(--space-2); }
    .langs { gap: 0 var(--space-4); }
}
