:root {
    --ink: #3b2f1e;
    --ink-light: #6b5d48;
    --parchment: #f5f0e1;
    --parchment-dark: #ebe3cc;
    --gold: #e8c45a;
    --gold-dark: #b89635;
    --rust: #a04830;
    --moss: #5a7a3a;
    --rule: #d4c8a8;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    font-family: 'Inter', 'Source Sans 3', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    font-size: 1rem;
    line-height: 1.65;
    color: var(--ink);
    background-color: var(--parchment);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/><feColorMatrix values='0 0 0 0 0.23 0 0 0 0 0.18 0 0 0 0 0.12 0 0 0 0.07 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}
h1, h2, h3, h4 {
    font-family: 'EB Garamond', 'Cormorant Garamond', Georgia, serif;
    color: var(--ink);
    font-weight: 600;
    letter-spacing: -0.01em;
}
h1 { font-size: 2.5rem; line-height: 1.15; margin-bottom: 1rem; }
h2 { font-size: 1.75rem; line-height: 1.2; margin-top: 2rem; margin-bottom: 0.75rem; }
h3 { font-size: 1.25rem; line-height: 1.3; }
p { margin-bottom: 0.75rem; }
a { color: var(--ink); text-decoration-color: var(--gold-dark); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--ink); }
.meta { color: var(--ink-light); font-size: 0.875rem; }
time, .num { font-variant-numeric: tabular-nums; }

/* Layout */
main {
    max-width: 42rem;
    margin: 2rem auto;
    padding: 0 1.5rem;
    /* Universal wrap: prose pages render lots of user-typed content
       (Mastodon bios, post bodies, handles, raw URLs, reply context).
       Setting overflow-wrap on the page container cascades to every
       descendant via inheritance, so a single unspaced token can never
       push a card past the column width on narrow viewports. */
    overflow-wrap: anywhere;
}
/* Pages that show event cards get more horizontal room so the text
   column is not squeezed between datestamp and image. The search form
   stays at the same width whether or not there are results, so the
   layout doesn't jump when the result set is empty. */
main:has(.event-row),
main:has(.search-form) {
    max-width: 56rem;
}
.container-wide {
    max-width: 64rem;
    margin: 0 auto;
    padding: 0 1.5rem;
}

/* Navigation */
.nav {
    border-bottom: 1px solid var(--rule);
    background: var(--parchment-dark);
}
.nav-inner {
    max-width: 64rem;
    margin: 0 auto;
    padding: 0.85rem 1.5rem;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    /* Matches the inter-anchor margin inside .nav-links so the
       Oppsanket → avatar gap reads the same as Søk → Oppsanket. */
    gap: 1.5rem;
}
/* Logo + nav-links cluster left as "site navigation"; the avatar
   gets pushed to the right edge as the distinct "account" surface.
   Auto margin on .nav-user eats the slack between the two groups.
   For anonymous visitors (no avatar), .nav-anon-actions takes the
   same role — Logg inn / Registrer pin to the right edge. */
.nav-user, .nav-anon-actions { margin-left: auto; }
.nav-logo {
    font-family: 'EB Garamond', Georgia, serif;
    font-weight: 600;
    font-size: 1.5rem;
    color: var(--ink);
    text-decoration: none;
    letter-spacing: 0.02em;
}
.nav-links a {
    color: var(--ink);
    text-decoration: none;
    margin-left: 1.5rem;
    font-size: 0.95rem;
    border-bottom: 1px solid transparent;
    transition: border-color 0.1s;
}
.nav-links a:first-child { margin-left: 0; }
.nav-links a:hover {
    border-bottom-color: var(--gold-dark);
}
/* Pending-forespørsler counter next to Oppsanket. Inline so it sits
   on the same baseline as the link text; gold-dark fill matches the
   nav hover underline so the two affordances read as related. */
.nav-badge {
    display: inline-block;
    min-width: 1.25rem;
    padding: 0 0.4rem;
    background: var(--gold-dark);
    color: var(--paper);
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.25rem;
    text-align: center;
    vertical-align: 1px;
}
/* User menu: <details> disclosure with the avatar (or text handle
   fallback) as the toggle and account links + logout inside the
   popup. No-JS native, mirrors the same pattern as .share-dropdown.
   Lives as a sibling of .nav-links (not inside it) so it stays
   visible on mobile next to the burger when .nav-links collapses. */
.nav-user {
    position: relative;
}
.nav-user-summary {
    cursor: pointer;
    list-style: none;
    color: var(--ink-light);
    font-size: 0.9rem;
    font-family: 'Inter', system-ui, sans-serif;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}
.nav-user-summary::-webkit-details-marker { display: none; }
.nav-user-summary::after {
    content: "\25BE"; /* ▾ */
    font-size: 0.75em;
    opacity: 0.7;
}
/* Hover affordance lives on the avatar (gold ring) when there is
   one; the text-handle fallback (no personal organizer) gets a
   caret-only hint. */
.nav-user-summary:hover .avatar {
    border-color: var(--gold-dark);
}
/* Header at the top of the popup: large display name + muted
   handle, separated from the menu items by a thin rule. */
.nav-user-header {
    padding: 0.4rem 0.6rem 0.5rem;
    margin-bottom: 0.25rem;
    border-bottom: 1px solid var(--rule);
}
.nav-user-name {
    font-weight: 600;
    color: var(--ink);
    font-size: 0.95rem;
    line-height: 1.3;
}
.nav-user-handle {
    color: var(--ink-light);
    font-size: 0.8rem;
    line-height: 1.3;
    /* Single line for the handle — long handles overflow with ellipsis
       rather than wrapping to a second line, which had been crowding
       the popup vertically. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.nav-user-menu {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    /* Widened from 160px so "Mine arrangementer" and longer fediverse
       handles fit on a single line. Items + handle also drop their
       wrap behaviour below — nowrap means widest item dictates the
       popup width up to a reasonable cap. */
    min-width: 240px;
    max-width: 340px;
    background: var(--parchment-dark);
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 0.4rem;
    z-index: 20;
    box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.nav-user-form { margin: 0; padding: 0; }
.nav-user-item, button.nav-user-item {
    display: block;
    width: 100%;
    margin: 0;
    padding: 0.4rem 0.6rem;
    text-align: left;
    text-decoration: none;
    /* Single line per menu item — keeps "Mine arrangementer" from
       wrapping inside the popup; popup widens up to its max-width. */
    white-space: nowrap;
    background: none;
    border: none;
    border-radius: 3px;
    color: var(--ink);
    font: inherit;
    font-size: 0.95rem;
    cursor: pointer;
}
.nav-user-item:hover, button.nav-user-item:hover {
    background: var(--parchment);
    color: var(--ink);
}
/* Visual rule between Innstillinger and Logg ut: the destructive
   action sits in its own block so a slip of the finger / cursor
   can't easily land on it. */
.nav-user-divider {
    border: none;
    border-top: 1px solid var(--rule);
    margin: 0.35rem 0;
}
/* No burger — the nav has at most three top-level items (Søk,
   Oppsanket, avatar) and the avatar holds the rest in its popup, so
   there's nothing to collapse. nav-inner's flex-wrap handles the
   narrow-screen case by letting items wrap to a second row if the
   instance name + nav items overflow. */
/* Narrow-phone tightening so the avatar stays on the same line as
   the rest of the nav. Default desktop spacing (gap 1.5rem, anchor
   margin 1.5rem, outer padding 1.5rem) eats too much horizontal
   budget on a 360–400px viewport once the instance name is more
   than a few characters. */
@media (max-width: 480px) {
    .nav-inner {
        padding-left: 0.85rem;
        padding-right: 0.85rem;
        /* Asymmetric spacing on phones: gap between flex groups
           (logo / nav-links / avatar) stays roomy at 1.25rem so the
           logo reads as the brand mark, while the inter-anchor
           margin inside .nav-links shrinks to 0.5rem to keep the
           whole row inside a 375px viewport. */
        gap: 1.25rem;
    }
    .nav-links a { margin-left: 0.5rem; }
}

/* Installed-PWA app mode on touch devices: the user has explicitly
   opted into the app experience, so we relocate the nav from the top
   to a fixed bottom bar — thumb-reachable, app-conventional, and frees
   the top for content. The logo drops entirely; "Søk" doubles as the
   home affordance (tap to return to the root listing), and the avatar
   menu sits at the right edge as the "you" tab. Gated on pointer:
   coarse as well as display-mode: standalone, so phones and tablets
   get the bottom bar at any width while a desktop-installed PWA (fine
   pointer) and in-browser viewers keep the familiar top nav. */
@media (display-mode: standalone) and (pointer: coarse) {
    .nav {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 10;
        border-bottom: none;
        border-top: 1px solid var(--rule);
        /* iOS home-indicator clearance — without this, the bottom row
           of nav items overlaps the gesture bar on notched devices. */
        padding-bottom: env(safe-area-inset-bottom);
    }
    .nav-logo { display: none; }
    /* Promote nav-links' anchors to direct flex children of
       .nav-inner so Søk / Oppsanket / avatar distribute as three
       evenly-spaced tabs instead of a left-clustered group + right
       avatar. Resets the inter-anchor margin too, since spacing now
       comes from justify-content. */
    .nav-inner {
        justify-content: space-around;
        gap: 0.5rem;
        /* Tighter than the default 0.85rem so the bar sits closer to
           native iOS tab-bar height (~49pt) rather than reading as a
           desktop nav that happens to be at the bottom. */
        padding-top: 0.5rem;
        padding-bottom: 0.5rem;
    }
    .nav-links { display: contents; }
    .nav-links a, .nav-links a:first-child { margin-left: 0; }
    /* Cancel the auto-margin that pinned the avatar/anon-actions to
       the right edge; with space-around they distribute on their own. */
    .nav-user, .nav-anon-actions { margin-left: 0; }
    /* Avatar drops from 32px to 28px in this mode — better balanced
       against the tightened bar, and closer to native tab-bar icon
       proportions. */
    .nav-user-summary .avatar {
        width: 28px;
        height: 28px;
    }
    /* Popup must open upward when the toggle sits at the bottom of
       the viewport — otherwise the menu renders off-screen. */
    .nav-user-menu {
        top: auto;
        bottom: calc(100% + 4px);
    }
    /* Keep the footer above the fixed bar. ~2.75rem covers nav-inner's
       0.5rem vertical padding + the 28px avatar; safe-area adds the
       home-indicator clearance on top of that. */
    body {
        padding-bottom: calc(2.75rem + env(safe-area-inset-bottom));
    }
    /* Post / message detail pages (../id/..) replace the "← Tilbake til
       tidslinjen" link with a fixed × in the upper right — same
       close-icon affordance as the lightbox. Stays put when the user
       scrolls through long threads, where a back link at the very top
       would be off-screen by the time it's wanted. */
    .back-link {
        position: fixed;
        top: calc(0.75rem + env(safe-area-inset-top));
        right: 0.75rem;
        z-index: 20;
        width: 2.4rem;
        height: 2.4rem;
        border-radius: 50%;
        background: var(--parchment-dark);
        border: 1px solid var(--rule);
        color: var(--ink);
        text-decoration: none;
        font-size: 0; /* hides the original anchor text */
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .back-link::before {
        content: "×";
        font-size: 1.75rem;
        line-height: 1;
    }
    /* Collapse the empty <p> wrapper so the now-fixed link doesn't
       leave a blank line at the top of the article. */
    p:has(> .back-link) {
        margin: 0;
        height: 0;
    }
}

/* Footer */
.footer {
    max-width: 64rem;
    margin: 2rem auto 1.5rem;
    padding: 1rem 1.5rem 0;
    border-top: 1px solid var(--rule);
    text-align: center;
    color: var(--ink-light);
    font-size: 0.875rem;
}
.footer p { margin: 0; }

/* Page header */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 1rem;
    margin-bottom: 2rem;
    flex-wrap: wrap;
}
.page-header h1 { margin-bottom: 0.25rem; }
.page-header .meta { margin: 0; }

/* Avatar + name-stack inside the organizer page header. Avatar to the
   left, h1 + handle-meta in a vertical stack on the right. Only used
   on personal organizer profiles (the avatar is suppressed for shared
   organizers, in which case this still collapses cleanly to text). */
.organizer-identity {
    display: flex;
    align-items: center;
    gap: 1rem;
    /* Flex items default to min-width: auto, which means an unbreakable
       token (a long `@user@instance.tld` handle on a remote profile)
       refuses to shrink and overflows the column — the universal
       overflow-wrap on <main> can't take effect. Set min-width: 0 here
       and on the name/meta stack so both flex layers (.page-header →
       .organizer-identity → inner div) can shrink and let the wrap
       cascade through. */
    min-width: 0;
}
.organizer-identity > div { min-width: 0; }
/* When the avatar is in the header, switch the page-header's baseline
   from flex-end (heading-text + button share a bottom edge — the
   pre-avatar default) to center, so the actions button vertically
   centers against the middle of the avatar instead of dangling at its
   bottom. Avatar-less pages keep the original alignment. */
.page-header:has(.organizer-identity) {
    align-items: center;
}

/* One-line page description rendered directly under the admin /
   instance sub-nav. The page heading (if any) is an h2 — same size
   as in-section headings so the page doesn't visually outweigh its
   contents. .page-headline intentionally has no margin override:
   the default h2 margin-top:2rem applies, matching inner-section
   h2 spacing on pages like /admin/stats and /admin/tags. */
.page-description {
    margin: 0.5rem 0 0.1rem 0;
    font-size: 0.85rem;
    color: var(--ink-light);
}
.page-header-actions {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
}
/* Profile page Follow / Unfollow / Friend forms in the page header. */
.profile-action-form { margin: 0; }
.profile-friend-pending { font-style: italic; }

/* Share dropdown (uses <details>) */
.share-dropdown {
    position: relative;
}
.share-dropdown > summary {
    cursor: pointer;
    list-style: none;
}
.share-dropdown > summary::-webkit-details-marker { display: none; }
.share-dropdown-menu {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    min-width: 240px;
    background: var(--parchment-dark);
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 0.5rem;
    z-index: 20;
    box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
/* When the trigger wraps to the left edge of its row on a narrow
   screen, the default right:0 menu overflows off the left of the
   viewport. yplev.js measures on open and toggles this class to
   anchor the menu to the trigger's left edge instead. */
.share-dropdown-menu--flip {
    right: auto;
    left: 0;
}
.share-dropdown-menu ul {
    list-style: none;
    padding: 0;
    margin: 0;
}
/* Every interactive row in the dropdown — Mastodon links, Bluesky link, the
   copy-link button — should look identical: full-width, left-aligned,
   transparent background, light hover. */
.share-dropdown-menu a,
.share-dropdown-menu .share-copy {
    display: block;
    width: 100%;
    padding: 0.4rem 0.6rem;
    text-align: left;
    text-decoration: none;
    background: none;
    border: none;
    border-radius: 3px;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.share-dropdown-menu a:hover,
.share-dropdown-menu .share-copy:hover {
    background: var(--parchment);
    color: var(--ink);
}
.share-dropdown-menu .meta {
    font-size: 0.85rem;
    opacity: 0.75;
}
.share-external-row,
.share-copy-row {
    margin-top: 0.4rem;
    padding-top: 0.4rem;
    border-top: 1px solid var(--rule);
}
.share-external-row:first-child,
.share-copy-row:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
/* Notis / Artikkel at the top of the share dropdown. Each link gets
   its own row (matches the existing fediverse / Bluesky / copy entries
   below) — the inherited .share-dropdown-menu a rule already makes
   each anchor a full-width block. An explicit bottom rule separates
   the compose group from the share targets below since those don't
   carry a border-top of their own. */
.share-compose-row {
    padding-bottom: 0.4rem;
    margin-bottom: 0.4rem;
    border-bottom: 1px solid var(--rule);
}

/* OAuth providers (login/register) */
.oauth-providers {
    margin-bottom: 1rem;
}
.oauth-form .form-actions {
    margin-top: 0.5rem;
}
.oauth-or {
    text-align: center;
    margin: 1rem 0;
    position: relative;
    color: var(--ink-light);
    font-size: 0.9rem;
}
.oauth-or::before,
.oauth-or::after {
    content: "";
    position: absolute;
    top: 50%;
    width: calc(50% - 2rem);
    border-top: 1px solid var(--rule);
}
.oauth-or::before { left: 0; }
.oauth-or::after { right: 0; }

.settings-manual-add {
    margin-top: 1.5rem;
}
.settings-manual-add > summary {
    cursor: pointer;
    color: var(--ink-light);
    font-size: 0.95rem;
    margin-bottom: 0.5rem;
}

/* Card */
.card {
    background: var(--parchment-dark);
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 1.25rem 1.5rem;
    margin-bottom: 1rem;
    position: relative;
}
.card::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 2px;
    background: var(--gold);
    border-radius: 4px 4px 0 0;
}
.card h3 {
    margin: 0 0 0.25rem;
    font-size: 1.25rem;
}
.card h3 a {
    color: var(--ink);
    text-decoration: none;
}
.card h3 a:hover {
    text-decoration: underline;
    text-decoration-color: var(--gold-dark);
    text-underline-offset: 3px;
}
.card .meta { margin: 0 0 0.75rem; }
.card > *:last-child { margin-bottom: 0; }

/* Buttons */
.btn, button {
    display: inline-block;
    padding: 0.55rem 1.25rem;
    font-family: inherit;
    font-size: 1rem;
    font-weight: 500;
    line-height: 1.4;
    color: var(--ink);
    background: var(--gold);
    border: 1px solid var(--gold-dark);
    border-radius: 3px;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.1s ease;
}
.btn:hover, button:hover {
    background: var(--gold-dark);
    color: var(--parchment);
}
.btn-secondary, button.btn-secondary {
    background: transparent;
    border-color: var(--ink);
    color: var(--ink);
}
.btn-secondary:hover, button.btn-secondary:hover {
    background: var(--ink);
    color: var(--parchment);
}
.btn-danger, button.btn-danger {
    background: var(--rust);
    border-color: var(--rust);
    color: var(--parchment);
}
.btn-danger:hover, button.btn-danger:hover {
    background: #8a3c28;
}
/* Form-action button styled to look like a regular link.
   Used when a textual link must POST (e.g. "Glemt passord?" that
   carries the already-typed identifier so the user doesn't re-type
   on the next page). */
.inline-form { display: inline; }
.link-button, button.link-button {
    background: none;
    border: none;
    padding: 0;
    color: var(--ink);
    text-decoration: underline;
    text-decoration-color: var(--gold-dark);
    text-underline-offset: 3px;
    cursor: pointer;
    font-size: inherit;
    font-family: inherit;
    font-weight: inherit;
}
.link-button:hover, button.link-button:hover {
    background: none;
    color: var(--gold-dark);
}

/* Form fields */
.field { margin-bottom: 1.25rem; }
.field label {
    display: block;
    font-weight: 500;
    margin-bottom: 0.35rem;
    color: var(--ink);
}
.field input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="hidden"]):not([type="image"]):not([type="range"]):not([type="color"]),
.field textarea,
.field select {
    width: 100%;
    padding: 0.55rem 0.75rem;
    font-family: inherit;
    font-size: 1rem;
    color: var(--ink);
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 3px;
    transition: border-color 0.1s, box-shadow 0.1s;
}
.field input:focus,
.field textarea:focus,
.field select:focus {
    outline: none;
    border-color: var(--gold-dark);
    box-shadow: 0 0 0 3px rgba(232, 196, 90, 0.25);
}
.field textarea {
    resize: vertical;
    min-height: 6rem;
}
.field-help {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.875rem;
    color: var(--ink-light);
}
/* Per-field validation error rendered inline by PATCH_SUBMIT_SCRIPT. */
.field-error {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.875rem;
    color: var(--rust);
}
/* Form-level status (success or generic error) revealed by the
   JS submit helper. Hidden via the [hidden] attribute initially. */
.form-status {
    display: block;
    margin-top: 0.75rem;
    padding: 0.5rem 0.75rem;
    border-radius: 4px;
    font-size: 0.9rem;
}
.form-status[hidden] { display: none; }
.form-status-ok { background: var(--parchment-dark); color: var(--ink); border-left: 3px solid var(--moss); }
.form-status-err { background: var(--parchment-dark); color: var(--ink); border-left: 3px solid var(--rust); }
/* Two related inputs sharing one row (lat/lng, width/height, etc.). */
.field-row {
    display: flex;
    gap: 0.5rem;
}
.field-row > * { flex: 1; min-width: 0; }

/* Form actions */
.form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
    margin-top: 1.5rem;
}
/* Login page: identifier input + submit on one line — bypasses the
   .form-actions divider so the page reads as one compact entry
   rather than form-then-button. */
.login-row {
    display: flex;
    gap: 0.5rem;
    align-items: stretch;
}
.login-row input { flex: 1 1 auto; }
.login-row button { flex: 0 0 auto; }

/* Avatar */
.avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 2px solid var(--ink);
    background: var(--parchment-dark);
    object-fit: cover;
}
.avatar-sm { width: 32px; height: 32px; border-width: 1px; }
.avatar-lg { width: 96px; height: 96px; border-width: 3px; }
/* Wrap for the avatar preview in /my/settings — relative positioning
   gives the .image-remove-x button (form-submit, image_action=remove)
   something to anchor against. Sized inline-block to the avatar
   circle so the X sits flush against the avatar's edge. */
.avatar-preview-wrap {
    position: relative;
    display: inline-block;
    margin-bottom: 1rem;
}

/* Datestamp */
.datestamp {
    display: inline-block;
    width: 56px;
    text-align: center;
    border: 2px solid var(--ink);
    border-radius: 4px;
    background: var(--parchment);
    font-variant-numeric: tabular-nums;
    overflow: hidden;
    flex-shrink: 0;
}
.datestamp-month {
    background: var(--ink);
    color: var(--parchment);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    padding: 0.15rem 0;
}
.datestamp-day {
    font-family: 'EB Garamond', Georgia, serif;
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--ink);
    padding: 0.25rem 0;
    line-height: 1.1;
}

/* Event row */
.event-row { display: flex; gap: 1rem; align-items: flex-start; }
.event-body { flex: 1; min-width: 0; }
.event-body h3 { margin-top: 0; }
.event-body .meta { margin-bottom: 0; }

/* Alerts */
.alert {
    padding: 0.75rem 1rem;
    border-radius: 3px;
    margin-bottom: 1rem;
    border-left: 3px solid;
}
.alert-info { background: rgba(212, 200, 168, 0.3); border-left-color: var(--ink-light); color: var(--ink); }
.alert-success { background: rgba(90, 122, 58, 0.12); border-left-color: var(--moss); color: var(--ink); }
.alert-warning { background: rgba(232, 196, 90, 0.18); border-left-color: var(--gold-dark); color: var(--ink); }
.alert-danger { background: rgba(160, 72, 48, 0.12); border-left-color: var(--rust); color: var(--ink); }

/* Skillelinje */
.rule { border: none; height: 1px; background: var(--rule); margin: 2rem 0; }
/* Section-closer rule used on /my/settings and /instance. Asymmetric
   margin (small top, normal bottom) makes the rule visually belong
   to the section above it — "below the button" — instead of feeling
   like it's introducing the section below. */
.section-end { border: none; height: 1px; background: var(--rule); margin: 0.5rem 0 2rem; }

/* Badges */
.badge {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    font-size: 0.75rem;
    font-weight: 500;
    letter-spacing: 0.03em;
    border-radius: 2px;
    background: var(--parchment-dark);
    color: var(--ink);
    border: 1px solid var(--rule);
    vertical-align: middle;
}
.badge-gold { background: var(--gold); border-color: var(--gold-dark); }
.badge-muted { background: transparent; color: var(--ink-light); font-style: italic; }
/* Unread-conversation count next to "Se forslag →"-link on invitation
   cards. Uses the rust accent so it reads as "needs attention" without
   shouting like a red dot. */
.badge-unread {
    background: var(--rust);
    color: var(--parchment);
    border-color: var(--rust);
    font-weight: 600;
    min-width: 1.4rem;
    text-align: center;
}

/* Tag chips */
.tag-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.4rem 0 0.25rem; }
.tag-chip {
    display: inline-block;
    padding: 0.15rem 0.6rem;
    font-size: 0.8rem;
    background: var(--parchment);
    color: var(--ink);
    border: 1px solid var(--rule);
    border-radius: 999px;
    text-decoration: none;
}
a.tag-chip:hover {
    border-color: var(--ink-light);
}

/* Search form */
.search-form {
    margin: 0 0 1.5rem;
    padding: 1rem 1.25rem;
    background: var(--parchment-dark);
    border: 1px solid var(--rule);
    border-radius: 4px;
}
.search-row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: end; }
.search-row .field { margin-bottom: 0; }
.search-row .field-grow { flex: 1 1 240px; }
/* Gap below the people-search row so results don't crowd the button. */
.friends-search-form { margin: 0.5rem 0 1.75rem; }
.coordinate-row { display: flex; gap: 0.5rem; align-items: stretch; flex-wrap: wrap; }
.coordinate-row input { flex: 1 1 240px; }

/* Production form — each showing row reads as a discrete block.
   Light rule above + breathing room so the eye lands on row
   boundaries without the row needing its own card-shaped frame
   (which would compete with the surrounding form's hierarchy). */
.showings-list { margin-bottom: 0.75rem; }
.showing-row {
    padding: 1rem 0;
    border-top: 1px solid var(--rule);
}
.showing-row:first-child { border-top: none; padding-top: 0; }
.showing-row .field:last-of-type { margin-bottom: 0.5rem; }

/* Preview of the production's current/just-picked image. When an
   image is set, the upload/URL fields are hidden and only this
   preview shows; the X overlay (form-submit, image_action=remove)
   is the path back to "no image set". Without an image, the wrap
   is hidden via the [hidden] attribute and the fields render. */
.image-preview-wrap {
    position: relative;
    display: inline-block;
    margin-bottom: 1rem;
}
/* The class selector above wins over the user-agent's
   [hidden] { display: none }, so we have to re-state hidden
   explicitly. Without this the wrap (and the X overlay inside it)
   stays visible even when the form has no image set. */
.image-preview-wrap[hidden] { display: none; }
.image-preview {
    display: block;
    max-width: 480px;
    max-height: 480px;
    height: auto;
    width: auto;
    border: 1px solid var(--rule);
    border-radius: 3px;
    background: var(--parchment-dark);
}
.image-remove-x {
    position: absolute;
    top: -0.5rem;
    right: -0.5rem;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    line-height: 1;
    font-size: 1.25rem;
    font-family: inherit;
    color: var(--parchment);
    background: var(--rust);
    border: 1px solid var(--rust);
    border-radius: 50%;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.image-remove-x:hover { background: #8a3c28; }
.search-tags-disclosure {
    margin-top: 1rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--rule);
}
.search-tags-disclosure summary {
    cursor: pointer;
    font-weight: 500;
    padding: 0.15rem 0;
    user-select: none;
    list-style: none;
}
.search-tags-disclosure summary::-webkit-details-marker { display: none; }
.search-tags-disclosure summary::before {
    content: "▸";
    display: inline-block;
    margin-right: 0.4rem;
    color: var(--ink-light);
    transition: transform 0.15s;
}
.search-tags-disclosure[open] summary::before {
    transform: rotate(90deg);
}
.search-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.75rem; }
.tag-chip-toggle {
    position: relative;
    display: inline-block;
    padding: 0.15rem 0.6rem;
    font-size: 0.8rem;
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 999px;
    cursor: pointer;
    user-select: none;
}
.tag-chip-toggle input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 1px;
    height: 1px;
}
.tag-chip-toggle:focus-within {
    box-shadow: 0 0 0 3px rgba(232, 196, 90, 0.3);
}
.tag-chip-toggle:has(input:checked) {
    background: var(--gold);
    border-color: var(--gold-dark);
    color: var(--ink);
}
.tag-chip-count { opacity: 0.6; font-variant-numeric: tabular-nums; }
.date-presets {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0.6rem 0 0.25rem;
}
.date-preset {
    padding: 0.2rem 0.7rem;
    font-family: inherit;
    font-size: 0.85rem;
    background: var(--parchment);
    color: var(--ink-light);
    border: 1px solid var(--rule);
    border-radius: 999px;
    cursor: pointer;
    line-height: 1.3;
    transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.date-preset:hover {
    border-color: var(--ink-light);
    color: var(--ink);
}
.date-preset.active {
    background: var(--gold);
    border-color: var(--gold-dark);
    color: var(--ink);
}
.search-actions {
    margin-top: 0.75rem;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}
.search-actions select {
    padding: 0.4rem 0.6rem;
    font-family: inherit;
    font-size: 0.95rem;
    color: var(--ink);
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 3px;
}
.search-actions select:focus {
    outline: none;
    border-color: var(--gold-dark);
    box-shadow: 0 0 0 3px rgba(232, 196, 90, 0.25);
}
@media (max-width: 640px) {
    .search-reset { display: none; }
}

/* "Datoer" heading paired with the Vis/Skjul tidligere toggle. The
   row carries the h2's top margin so the toggle baseline-aligns with
   the heading instead of being pushed down by it. */
.dates-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    flex-wrap: wrap;
    margin-top: 2rem;
}
.dates-header h2 { margin-top: 0; }
.dates-toggle {
    font-size: 0.85rem;
    color: var(--ink-light);
    white-space: nowrap;
}

/* Event thumb + production hero */
.event-row { display: grid; grid-template-columns: auto 1fr auto; gap: 1.25rem; align-items: start; }
.event-thumb {
    display: block;
    width: 200px;
    line-height: 0;
    border-radius: 3px;
    overflow: hidden;
    align-self: stretch;
}
.event-thumb img {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: cover;
    aspect-ratio: 4/3;
}
.production-hero {
    margin: 1rem 0 1.5rem;
    max-height: 420px;
    overflow: hidden;
    border-radius: 4px;
}
.production-hero img {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
}
@media (max-width: 720px) {
    .event-row { grid-template-columns: auto 1fr; }
    .event-thumb {
        grid-column: 1 / -1;
        grid-row: 1;
        width: 100%;
        margin-bottom: 0.5rem;
    }
    .event-thumb img { aspect-ratio: 16/9; }
}

/* Ticket link sits in the third grid column of the showing card
   (upper-right). Self-aligns so it doesn't stretch to fill the row
   and stays compact next to the date + body block. The same
   `.event-row` grid is used by listing cards with thumbnails on the
   right; ticket-link only renders on showing cards (no thumb), so
   the two never collide. */
.ticket-link {
    margin: 0;
    align-self: start;
    justify-self: end;
    white-space: nowrap;
}
/* Narrow viewport: ticket link drops to its own row below body so
   title + date have full width to themselves. Defined here, after
   the default above, so it actually wins the cascade. */
@media (max-width: 720px) {
    .ticket-link {
        grid-column: 1 / -1;
        justify-self: start;
        margin-top: 0.5rem;
    }
}
/* `.btn`'s `display: inline-block` beats UA `[hidden] { display: none }`
   by specificity, so the show/hide-on-RSVP toggle on the invitation
   page would render the link even when the `hidden` attribute is set.
   Restate it here so the attribute does its job. */
.ticket-link[hidden] { display: none; }

/* Showing map (Leaflet) */
.showing-map {
    height: 220px;
    margin: 0.75rem 0;
    border: 1px solid var(--rule);
    border-radius: 3px;
    overflow: hidden;
}
.showing-map .leaflet-container {
    background: var(--parchment-dark);
    font-family: inherit;
}

/* Calendar subscribe block — flows inline under the Settings h2
   like the rest of the Settings sections. The wrapper class stays
   only for the inline <code> URL-chip selector below. */
.calendar-subscribe code {
    display: inline-block;
    padding: 0.1rem 0.4rem;
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 3px;
    font-size: 0.8rem;
    word-break: break-all;
}

/* Empty state */
.empty-state {
    text-align: center;
    padding: 2.5rem 1.5rem;
    color: var(--ink-light);
    background: var(--parchment-dark);
    border: 1px dashed var(--rule);
    border-radius: 4px;
}
.empty-state p { margin-bottom: 1rem; }

/* Data table — for admin/config tabular data. See design-guide.md. */
.data-table {
    width: 100%;
    border-collapse: collapse;
    margin: 0 0 1rem;
    font-size: 0.95rem;
}
.data-table thead th {
    text-align: left;
    font-weight: 500;
    font-size: 0.8rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-light);
    padding: 0.4rem 0.75rem;
    border-bottom: 1px solid var(--rule);
}
.data-table tbody td {
    padding: 0.55rem 0.75rem;
    border-bottom: 1px solid var(--rule);
    vertical-align: middle;
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table .num {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.data-table .row-actions {
    text-align: right;
    white-space: nowrap;
}
.data-table code {
    font-size: 0.875rem;
    background: var(--parchment-dark);
    padding: 0.05rem 0.35rem;
    border-radius: 2px;
}

/* Compact button variant for inline actions in tables and lists. */
.btn-sm, button.btn-sm {
    padding: 0.25rem 0.7rem;
    font-size: 0.85rem;
    line-height: 1.3;
}

/* No-JS confirm via <details>. First click opens the form; the actual
   POST happens from the inner button. Replaces onclick=confirm(). */
.confirm-delete > summary {
    cursor: pointer;
    list-style: none;
    display: inline-block;
}
.confirm-delete > summary::-webkit-details-marker { display: none; }
.confirm-delete[open] > summary { display: none; }
.confirm-delete-body {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}
.confirm-delete-prompt {
    font-size: 0.85rem;
    color: var(--ink-light);
    font-style: italic;
}

/* Generic alignment utility for inline action rows. */
.text-right { text-align: right; }

/* Friend rows: name + handle on the left, action button on the right,
   all on one line. The action button uses .btn-mini-danger which
   matches the discreet rsvp toggle visual: transparent default, hover
   reveals border + colour. */
.friend-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
.friend-row p { margin: 0; flex: 1 1 auto; }
.friend-row form { margin: 0; }
/* Several actions on one row (e.g. friend-request + follow on a
   search result) sit together, tight, and wrap on a narrow screen. */
.friend-row-actions {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
    justify-content: flex-end;
}
.friend-row-actions form { margin: 0; }
/* All friends inside one card; rows separated by a thin rule
   instead of each having its own card border. The vertical padding
   per row keeps the Fjern hit-target comfortable. */
.friends-list-card .friend-row {
    padding: 0.6rem 0;
    border-top: 1px solid var(--rule);
}
.friends-list-card .friend-row:first-child { border-top: 0; padding-top: 0; }
.friends-list-card .friend-row:last-child { padding-bottom: 0; }
.btn-mini-danger {
    font-size: 0.85rem;
    padding: 0.15rem 0.45rem;
    background: transparent;
    border: 1px solid transparent;
    color: var(--ink-light);
    cursor: pointer;
    border-radius: 0.25rem;
    line-height: 1.3;
}
.btn-mini-danger:hover {
    color: #c53030;
    border-color: currentColor;
}

/* Discreet card-level RSVP toggles. Inactive: muted text-only.
   Active (aria-pressed=true): coloured to match the full-size buttons
   (moss for kommer, gold for interessert) so the visual language is
   consistent across surfaces. */
.rsvp-form-card {
    display: inline-flex; gap: 0.4rem; margin-top: 0.5rem;
    padding: 0;
}
.rsvp-form-card button {
    font-size: 0.85rem; padding: 0.15rem 0.45rem;
    background: transparent; border: 1px solid transparent;
    color: var(--ink-light); cursor: pointer; border-radius: 0.25rem;
    line-height: 1.3;
}
.rsvp-form-card button:hover {
    color: var(--ink); border-color: var(--ink-light);
}
.rsvp-form-card .rsvp-going-mini[aria-pressed="true"] {
    color: var(--moss); border-color: var(--moss); font-weight: 600;
}
.rsvp-form-card .rsvp-maybe-mini[aria-pressed="true"] {
    color: var(--gold-dark); border-color: var(--gold-dark); font-weight: 600;
}
/* The third state is invitation-list-only (not on public events).
   Rust accent matches the design's "decline / destructive" voice
   without going as loud as .btn-danger — this is a quiet "no". */
.rsvp-form-card .rsvp-declined-mini[aria-pressed="true"] {
    color: var(--rust); border-color: var(--rust); font-weight: 600;
}

/* Invitee section on the event-edit form. The chip-list above the
   textarea lists existing invitees as inline blocks (no bullets),
   each with an X overlay; below sit two collapsible details for
   the friend-picker and the freeform textarea. */
/* One invitee per line, mirrors the friend-picker row layout:
   name on the left grows to fill, badge + (optional) remove-X
   sit on the right. The row reads as content, not chrome. */
.invitee-list {
    list-style: none;
    padding: 0;
    margin: 0 0 1rem;
}
.invitee-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0;
    border-top: 1px solid var(--rule);
}
.invitee-row:first-child { border-top: none; }
.invitee-label { flex: 1 1 auto; min-width: 0; }
.invitee-add-section {
    margin-bottom: 0.75rem;
}
/* Sub-headline-style summary: serif, slightly smaller than h3,
   with the browser's native disclosure triangle visible in front
   so users immediately recognize the expand affordance. */
.invitee-add-section > summary {
    cursor: pointer;
    font-family: 'EB Garamond', 'Cormorant Garamond', Georgia, serif;
    font-weight: 600;
    font-size: 1.1rem;
    color: var(--ink);
    margin: 0.5rem 0 0.25rem;
}
.invitee-add-section > summary:hover {
    color: var(--gold-dark);
}
/* Friend picker rendered as a vertical list (one name per line)
   with a small "Legg til" button at the end of each row. Replaces
   the prior all-buttons row so friend names look like content,
   not chrome. */
.friend-picker-list {
    list-style: none;
    padding: 0;
    margin: 0.25rem 0 0.5rem;
}
.friend-picker-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.35rem 0;
    border-top: 1px solid var(--rule);
}
.friend-picker-row:first-child { border-top: none; }
/* Class selector beats UA `[hidden] { display: none }`, restate it. */
.friend-picker-row[hidden] { display: none; }
.friend-picker-name { flex: 1 1 auto; min-width: 0; }
/* Letter / note treatment for invitation cards on /my/events: the
   inviter is sending you a message that points at an event, so the
   outer container drops the standard .card chrome and reads as a
   personal note (left-edge gold rule, no box, no top stripe). The
   inner event card keeps its full chrome — that's the artifact
   being passed along. */
.invitation-card {
    background: transparent;
    border: none;
    border-radius: 0;
    padding: 0.5rem 0 1rem 1.25rem;
    border-left: 3px solid var(--gold);
    margin-bottom: 1.5rem;
}
.invitation-card::before { display: none; }

.invitation-card .invitation-from {
    margin: 0 0 0.5rem;
    font-size: 0.95rem;
    color: var(--ink-light);
    font-style: italic;
}

/* Detail-page header. Sits above the event card so it carries the
   invitation's personal voice — sized smaller than h1 so the event
   name (h1 inside the card below) stays the visual focal point. */
.page-header .invitation-from {
    margin: 0 0 0.5rem;
    font-size: 1.15rem;
    font-weight: 600;
}

.invitation-card .inviter-comment {
    margin: 0 0 0.85rem;
    padding: 0.25rem 0 0.25rem 0.85rem;
    border-left: 2px solid var(--rule);
    font-family: 'EB Garamond', 'Cormorant Garamond', Georgia, serif;
    font-size: 1.1rem;
    line-height: 1.45;
    color: var(--ink);
}

/* Inner event card — visually the focal artifact inside the note. */
.invitation-card .invitation-event {
    margin: 0 0 0.85rem;
}

.invitation-card .invitation-link {
    margin: 0.25rem 0 0;
    font-size: 0.9rem;
}
.invitation-card .invitation-link a {
    color: var(--ink-light);
}
.invitation-card .invitation-link a:hover {
    color: var(--ink);
}

/* Richer invitation card on the friends page: thumbnail-sized
   hero (don't compete with the page's own event-view), description
   and extra-dates collapsed under a details-summary so the RSVP
   buttons stay visible without scrolling. */
.invitation-card .invitation-hero {
    display: block;
    max-width: 240px;
    max-height: 240px;
    height: auto;
    width: auto;
    margin: 0.5rem 0 0.75rem;
    border: 1px solid var(--rule);
    border-radius: 3px;
}
.invitation-card .invitation-extra {
    margin: 0.4rem 0;
}
.invitation-card .invitation-extra > summary {
    cursor: pointer;
    color: var(--ink-light);
    font-size: 0.9rem;
}
.invitation-card .invitation-dates {
    list-style: none;
    padding: 0;
    margin: 0.25rem 0 0.5rem;
}
.invitation-card .invitation-dates > li {
    padding: 0.15rem 0;
    font-size: 0.9rem;
}
.invitation-card .invitation-description {
    margin-top: 0.4rem;
    font-size: 0.95rem;
}

/* Organizer roster on the event view page. Headings + plain
   lists, no browser bullets. Matches the parchment palette and
   leaves room for badge/handle/date metadata per row. */
.roster-bucket-title {
    font-size: 0.95rem;
    font-weight: 600;
    margin: 0.75rem 0 0.25rem;
    color: var(--ink);
}
.roster-bucket {
    list-style: none;
    padding: 0;
    margin: 0 0 0.75rem;
}
.roster-bucket > li {
    padding: 0.2rem 0;
    font-size: 0.9rem;
    border-top: 1px solid var(--rule);
}
.roster-bucket > li:first-child { border-top: none; }

/* Excerpt below event title in listings */
.event-body .excerpt {
    color: var(--ink);
    margin: 0.4rem 0 0;
    font-size: 0.95rem;
    line-height: 1.5;
}

/* Listing toggle (anchor-based pill nav) */
.listing-toggle {
    display: inline-flex;
    gap: 0.4rem;
    margin: 0 0 1.25rem;
}
.listing-toggle .pill {
    display: inline-block;
    padding: 0.3rem 0.85rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    background: var(--parchment);
    color: var(--ink-light);
    font-size: 0.85rem;
    text-decoration: none;
    transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.listing-toggle .pill:hover {
    border-color: var(--ink-light);
    color: var(--ink);
}
.listing-toggle .pill-active {
    background: var(--gold);
    border-color: var(--gold-dark);
    color: var(--ink);
}

/* City shortcut row */
.city-shortcuts {
    margin: 0 0 1.25rem;
    font-size: 0.9rem;
    color: var(--ink-light);
}
.city-shortcuts a { color: var(--ink); }

/* Pagination */
.pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
    justify-content: space-between;
    align-items: center;
    margin: 1.5rem 0 0.5rem;
    padding-top: 1rem;
    border-top: 1px solid var(--rule);
}
.pagination-summary {
    font-size: 0.875rem;
    color: var(--ink-light);
}
.pagination-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    align-items: center;
}
.pagination-link {
    display: inline-block;
    min-width: 2rem;
    padding: 0.25rem 0.6rem;
    text-align: center;
    font-size: 0.875rem;
    line-height: 1.4;
    color: var(--ink);
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 3px;
    text-decoration: none;
}
a.pagination-link:hover {
    border-color: var(--ink-light);
}
.pagination-current {
    background: var(--gold);
    border-color: var(--gold-dark);
    font-weight: 500;
}
.pagination-ellipsis {
    color: var(--ink-light);
    padding: 0 0.25rem;
}

/* Unverified email banner */
.unverified-banner {
    background: rgba(232, 196, 90, 0.18);
    border-bottom: 1px solid var(--gold-dark);
}
.unverified-banner-inner {
    max-width: 64rem;
    margin: 0 auto;
    padding: 0.5rem 1.5rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    font-size: 0.9rem;
}
.unverified-banner-form { margin: 0; }
.unverified-banner-form button { padding: 0.25rem 0.75rem; font-size: 0.85rem; }

/* Visibility pills (radio toggle group) */
.visibility-pills {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0.75rem 0 0.25rem;
    font-size: 0.875rem;
    color: var(--ink-light);
    flex-wrap: wrap;
}
.visibility-pills-label {
    margin-right: 0.25rem;
    font-style: italic;
}
.visibility-pills label {
    position: relative;
    cursor: pointer;
    line-height: 1;
}
.visibility-pills input[type="radio"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    width: 1px;
    height: 1px;
}
.visibility-pills .pill {
    display: inline-block;
    padding: 0.25rem 0.7rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    background: var(--parchment);
    color: var(--ink-light);
    font-size: 0.8rem;
    line-height: 1.3;
    transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.visibility-pills label:hover .pill {
    border-color: var(--ink-light);
    color: var(--ink);
}
.visibility-pills input[type="radio"]:checked + .pill {
    background: var(--gold);
    border-color: var(--gold-dark);
    color: var(--ink);
}
.visibility-pills input[type="radio"]:focus-visible + .pill {
    box-shadow: 0 0 0 3px rgba(232, 196, 90, 0.3);
    outline: none;
}

/* Narrow screens: bands break out of main's gutter and run edge-to-edge,
   while non-band content keeps a small inner gutter. */
@media (max-width: 480px) {
    main { padding: 0 0.75rem; }
    .card,
    .search-form,
    .empty-state {
        margin-left: -0.75rem;
        margin-right: -0.75rem;
        border-left: 0;
        border-right: 0;
        border-radius: 0;
        padding-left: 0.85rem;
        padding-right: 0.85rem;
    }
    .card {
        padding-top: 0.85rem;
        padding-bottom: 0.85rem;
        margin-bottom: 0.5rem;
    }
    .card::before { border-radius: 0; }
    .search-form {
        padding-top: 0.75rem;
        padding-bottom: 0.75rem;
    }
    .empty-state {
        padding-top: 2rem;
        padding-bottom: 2rem;
    }
}

/* Discreet text-style mini button — used for inline actions inside
   denser content (Svar / Slett on a message row) where a real button
   would dominate the line. Same physical size as a link, no border,
   underline on hover. */
.btn-mini-link {
    background: transparent;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    font-size: 0.85rem;
    color: var(--ink-light);
    cursor: pointer;
    text-decoration: none;
}
.btn-mini-link:hover {
    color: var(--ink);
    text-decoration: underline;
    text-decoration-color: var(--gold-dark);
    text-underline-offset: 2px;
}
.btn-mini-link-danger { color: var(--ink-light); }
.btn-mini-link-danger:hover {
    color: var(--rust);
    text-decoration-color: var(--rust);
}
.inline-form { display: inline; margin: 0; padding: 0; }

/* Invitation thread (under the conversation section on the
   invitation detail page). Two-level structure: a top-level message
   + a flat indented list of all descendants. Chevron is on the left
   side of the summary line so it reads naturally as "click to
   expand"; child messages sit indented with a left rule that visually
   ties them to the thread root. */
.invitation-threads {
    margin-top: 1rem;
}
.invitation-thread {
    border-top: 1px solid var(--rule);
    padding: 0.75rem 0 0.5rem;
}
.invitation-thread:first-of-type { border-top: 0; }

/* Disclosure chevron: a real <button class="invitation-thread-toggle">
   in the side column, under the avatar. Only rendered when the root
   has replies. JS toggles `invitation-thread-collapsed` on the parent
   .invitation-thread — the glyph and the replies' visibility key off
   that class. A thread card is a plain <div>, so nothing else in it
   collapses the thread; this button is the only affordance. */
.invitation-thread-toggle {
    display: inline-block;
    background: none;
    border: 0;
    color: var(--ink-light);
    cursor: pointer;
    line-height: 1;
    width: 32px;
    text-align: center;
    padding: 0.15rem 0;
    font-size: 1.1rem;
}
.invitation-thread-toggle:hover { color: var(--ink); }
.invitation-thread-toggle::before { content: "▾"; }
.invitation-thread-collapsed .invitation-thread-toggle::before { content: "▸"; }
.invitation-thread-collapsed .invitation-thread-replies { display: none; }

/* Each message: side column on the left (avatar + optional thread
   toggle stacked) and content on the right. The card chrome wraps
   both columns so the side sits on the same parchment-dark surface
   as the content. */
/* Layout: avatar floats top-left so the meta line wraps to its right;
   body and subsequent siblings clear the float, putting them on a new
   row at the container's left edge — same indent as the avatar. */
.invitation-message {
    display: block;
    margin: 0 0 0.5rem;
    padding: 0.5rem 0.75rem;
    background: var(--parchment-dark);
    border-radius: 3px;
}
.invitation-message::after { content: ""; display: block; clear: both; }
/* Narrow viewport: same edge-to-edge treatment as .card / .search-form
   (the rule lived in the earlier @media block but lost to the default
   above on cascade — same source-order trap that hit
   .invitation-message-attachments). Negative margin breaks out of
   main's 0.75rem gutter so the card's own 0.75rem padding is the only
   horizontal inset; border-radius drops at the page edge. */
@media (max-width: 480px) {
    .invitation-message {
        margin-left: -0.75rem;
        margin-right: -0.75rem;
        border-radius: 0;
    }
}
.invitation-message-aside {
    float: left;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    margin-right: 0.6rem;
}
.invitation-message-avatar {
    width: 32px;
    height: 32px;
    border-width: 1px;
    flex: 0 0 auto;
}
.invitation-message-content { min-width: 0; }
/* The meta line is the only element that wraps next to the avatar;
   everything else (body, attachments, link card, actions) clears below
   so it left-aligns with the avatar's left edge instead of indenting
   under it. */
.invitation-message-content > :not(.invitation-message-meta):not(.reply-context) {
    clear: left;
}
/* Reply-context preview rendered above the meta line on the flat
   timeline — a faint inline-quoted hint of the parent the reply is
   in reply to. Drives the "@alice: «excerpt»" line that gives a
   chronologically-ordered timeline its conversational footing
   without re-introducing threading. */
.reply-context,
a.reply-context {
    display: block;
    font-size: 0.8rem;
    color: var(--ink-light);
    margin: 0 0 0.2rem;
    padding-left: 0.5rem;
    border-left: 2px solid var(--rule);
    text-decoration: none;
}
a.reply-context:hover { color: var(--ink); }
.reply-context-author { font-weight: 600; }
.reply-context-excerpt { font-style: italic; }
.invitation-message-meta {
    margin: 0 0 0.2rem;
    font-size: 0.85rem;
    color: var(--ink-light);
}
.invitation-message-meta strong { color: var(--ink); }
/* May hold plain text (pre-wrap keeps yplev-authored newlines) or
   sanitized HTML (an inbound Mastodon Note's content — <p>, links,
   mentions). Child <p> margins are tightened so HTML bodies don't
   tower over plain-text ones. */
.invitation-message-body {
    margin: 0;
    color: var(--ink);
    white-space: pre-wrap;
}
.invitation-message-body p { margin: 0 0 0.4rem; }
.invitation-message-body > *:last-child { margin-bottom: 0; }
.invitation-message-deleted { color: var(--ink-light); font-style: italic; }
.invitation-message-actions {
    margin: 0.3rem 0 0;
    font-size: 0.85rem;
}
.invitation-message-actions > * + * { margin-left: 0.5rem; }

/* Replies — flat chronological list under the thread root, indented
   with a left rule so the parent/child relationship is clear without
   nesting deeper. */
.invitation-thread-replies {
    list-style: none;
    margin: 0.25rem 0 0;
    padding: 0 0 0 1.5rem;
    border-left: 2px solid var(--rule);
}
.invitation-thread-replies > li { margin: 0; }

/* Self-reply roll (Mastodon "🧵" thread): N consecutive posts by the
   same author rendered as a single card. Avatar + author meta appear
   once at the top; each part keeps its own #msg-uuid anchor for
   external permalinks. The focused part (the one the URL pointed at)
   gets a left-border highlight so the reader sees where they landed. */
.message-roll .message-roll-part {
    padding: 0.4rem 0;
    border-top: 1px dashed var(--rule);
}
.message-roll .message-roll-part:first-of-type { border-top: 0; padding-top: 0.25rem; }
.message-roll-part-focused {
    background: var(--parchment);
    border-left: 3px solid var(--gold);
    padding-left: 0.5rem;
    margin-left: -0.5rem;
}
.message-roll-part-marker {
    margin: 0 0 0.25rem;
    font-size: 0.75rem;
    color: var(--ink-light);
}

/* Compose form below the thread list (new top-level message) and the
   inline reply form that appears under a row when "Svar" is clicked.
   Compact textarea, send button on the right. */
/* Compose form: Send button sits inside the textarea's bottom-right
   corner so the input feels like a single element (chat-style)
   rather than form + button-row. Textarea reserves padding-bottom
   so user-typed text doesn't slide under the button. */
.invitation-message-compose {
    margin: 0.5rem 0;
    padding: 0;
    position: relative;
}
.invitation-message-compose textarea {
    width: 100%;
    box-sizing: border-box;
    font-family: inherit;
    font-size: 0.95rem;
    padding: 0.4rem 2.5rem 0.4rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 3px;
    background: var(--parchment);
    color: var(--ink);
    resize: vertical;
    min-height: 2.5rem;
    display: block;
}
.invitation-message-compose .form-actions {
    position: absolute;
    bottom: 0.45rem;
    right: 0.5rem;
    margin: 0;
    padding: 0;
    border: 0;
    justify-content: flex-end;
}
/* Compact icon-style send button. Sized to feel like a chat-input
   action: small ink-coloured glyph, hover lights it up. Larger
   touch target than the glyph alone via padding. */
.btn-send {
    background: transparent;
    border: 0;
    padding: 0.2rem 0.45rem;
    color: var(--ink-light);
    cursor: pointer;
    font-size: 1rem;
    line-height: 1;
    border-radius: 3px;
}
.btn-send:hover {
    color: var(--gold-dark);
}
.btn-send:disabled {
    color: var(--rule);
    cursor: default;
}
.invitation-message-reply-row {
    margin-left: 1.5rem;
}

/* Light theme — opt-in via the yplev_theme=light cookie, applied
   client-side by THEME_BOOTSTRAP_SCRIPT before first paint. Aimed
   at e-ink tablets, so the parchment palette and the SVG noise
   texture are dropped for a flat white surface. The ink/gold/rust
   accents stay; on greyscale e-ink they render as low-contrast
   greys against white, which is the desired look. */
html.theme-light {
    --parchment: #ffffff;
    --parchment-dark: #f5f5f5;
    --rule: #d0d0d0;
}
html.theme-light body {
    background-image: none;
}

/* Admin sub-navigation. Without these rules the three <a> items
   collapse into a single text run ("PeersTaggerByer"). */
.admin-subnav {
    display: flex;
    gap: 1rem;
    border-bottom: 1px solid var(--rule);
    margin-bottom: 1rem;
    padding-bottom: 0.25rem;
}
.admin-subnav-item {
    text-decoration: none;
    padding: 0.25rem 0.5rem;
    color: var(--ink);
}
.admin-subnav-item.is-active {
    border-bottom: 2px solid var(--gold-dark);
    font-weight: bold;
}

/* /admin index: clickable card per section. */
.admin-index {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 1rem;
}
.admin-index-card {
    display: block;
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 1rem;
    text-decoration: none;
    color: var(--ink);
}
.admin-index-card:hover {
    border-color: var(--gold-dark);
}

/* Curated-tags list on /admin/tags. One block per curated tag; the
   header row (slug / label / order / delete) stays compact, and the
   mapped-tags chip cloud takes the full block width below so long
   lists wrap naturally without a table column squeezing the slug
   and label. */
.curated-tag-list {
    list-style-position: outside;
    padding-left: 2.25rem;
    margin-bottom: 1rem;
}
.curated-tag-list > .curated-tag-block + .curated-tag-block {
    margin-top: 0.75rem;
}
.curated-tag-block {
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 0.65rem 0.85rem;
    background: var(--parchment-dark);
}
.curated-tag-block::marker {
    color: var(--ink-light);
}
.curated-tag-header {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-bottom: 0.5rem;
}
.curated-tag-title {
    flex: 1;
}
.curated-tag-title code {
    background: var(--parchment);
    padding: 0.05rem 0.3rem;
    border-radius: 3px;
    font-weight: 600;
}
.curated-tag-actions {
    margin-left: auto;
}
.curated-tag-empty {
    margin: 0;
}
.tag-chip-cloud {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.5rem;
}
.tag-chip code {
    padding: 0.1rem 0.4rem;
    background: var(--parchment);
    border-radius: 3px;
}

/* User activity timeline (/my/timeline). A follower's reply renders
   as a commented boost: the comment row (reusing .invitation-message)
   above an embedded event card, with fav/boost/reply controls and the
   flat reply thread beneath. */
.activity-filter {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1rem;
}
.activity-chip {
    text-decoration: none;
    padding: 0.2rem 0.6rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    color: var(--ink);
    font-size: 0.875rem;
}
.activity-chip-on {
    border-color: var(--gold-dark);
    background: var(--parchment-dark);
    font-weight: 600;
}
/* Sort selector inside the toolbar filter row. Native <select>
   styled to sit on the same line as the org chips, slightly
   compact so it doesn't dominate. The <noscript> submit button
   only renders when JS is off (the onchange auto-submit handles
   the common case). */
.activity-sort-form {
    display: inline-flex;
    gap: 0.4rem;
    align-items: center;
    margin: 0;
}
.activity-sort-form select {
    padding: 0.15rem 0.4rem;
    font-size: 0.875rem;
    font-family: inherit;
    color: var(--ink);
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 3px;
}
/* A timeline item — an event-reply or a post — is borderless like a
   forslag thread: the embedded event card or the post header sits
   above the shared <details> thread, separated by a hairline. No
   card chrome (its parchment-dark would swallow the message chips). */
.activity-item {
    border-top: 1px solid var(--rule);
    padding-top: 0.6rem;
    margin-top: 0.6rem;
}
.activity-section-heading + .activity-item,
.activity-filter + .activity-item { border-top: 0; }
/* Timeline bodies read at slightly smaller than default — a denser
   feed without losing readability. Scoped to .activity-item so the
   invitation page (same .invitation-message-body class) is unaffected. */
.activity-item .invitation-message-body,
.activity-item .post-body { font-size: 1em; }
/* "N svar · M delt · sist HH:MM" — single-line activity rollup
   beneath each timeline row, linking to the row's thread page.
   Muted by default; underlines on hover so it reads as actionable.
   Sits tight (margin-top 0) so the line reads as the post's footer
   rather than its own block. Used by the EventTopEntry row; post
   rows render the same content as part of .activity-row-footer
   (which also carries the Svar action). */
a.activity-hint {
    display: block;
    margin: 0;
    font-size: 0.8rem;
    color: var(--ink-light);
    text-decoration: none;
}
a.activity-hint:hover { text-decoration: underline; color: var(--ink); }
/* Aktivitet-feed jump-to-parent affordance — same visual weight as
   activity-hint, but appears under a row that is itself a non-root
   item (a reply or a boost), so the reader can drop into the parent
   conversation. */
.activity-thread-link {
    margin: 0.2rem 0 0;
    font-size: 0.8rem;
}
.activity-thread-link a { color: var(--ink-light); text-decoration: none; }
.activity-thread-link a:hover { text-decoration: underline; color: var(--ink); }
/* Post-row footer: Svar (left) and "N svar · M delt · sist HH:MM"
   (right) on one line. Each chip is a link — Svar to the reply
   page, the hint chip to the thread page. Lives outside the post's
   .invitation-message box so the action reads as a row-level
   affordance, not part of the body chrome. Sits flush against the
   box above. Flex with auto-margin on the hint chip handles all
   three populations: both / Svar-only / hint-only. */
.activity-row-footer {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    margin: 0;
    padding: 0.15rem 0.75rem 0;
    font-size: 0.8rem;
    color: var(--ink-light);
}
/* Tighten the post box's bottom padding when its row has a footer
   attached. The article's normal 0.5rem padding-bottom + the
   footer's 0.15rem padding-top stacked to ~0.65rem of whitespace
   between the body and the Svar/hint line — too airy. Descendant
   combinator (not `>`) because the article is nested inside
   .invitation-thread, not a direct child of the .activity-item. */
.activity-item .invitation-message.activity-post-header {
    padding-bottom: 0.2rem;
}
.activity-row-footer .activity-row-action {
    text-decoration: none;
    color: var(--ink-light);
}
.activity-row-footer .activity-row-action:hover {
    text-decoration: underline;
    color: var(--ink);
}
.activity-row-footer .activity-row-hint {
    margin-left: auto;  /* pushes the hint to the right edge */
}

/* Fav / boost toggle buttons in the post-row footer. Each is a tiny
   form (so the click submits via standard HTML POST, no JS). The
   button itself is styled to read as an inline chip alongside Svar:
   no border, no background, just a glyph and a count. The "on" state
   (viewer has faved or boosted) flips the glyph to its filled form
   and tints it with the gold accent. The form wrapper has no margin
   or chrome — it disappears into the flex row. */
.activity-row-interact-form {
    margin: 0;
    padding: 0;
    display: inline;
}
.activity-row-footer button.activity-row-interact {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
    color: var(--ink-light);
    font: inherit;
    line-height: inherit;
}
.activity-row-footer button.activity-row-interact:hover {
    color: var(--ink);
    text-decoration: underline;
}
.activity-row-footer .activity-row-interact-on {
    color: var(--gold-dark);
}
.activity-row-footer .activity-row-interact-on:hover {
    color: var(--gold-dark);
}
.activity-row-interact-count {
    font-variant-numeric: tabular-nums;
}
/* Detail-page booster-stats block: small heading + a list of boosters
   with avatar + name + handle + time. Renders inside .invitation-thread
   between the post header and the action footer, so it sits visually
   below the post body the same way the inline reply list does. */
.activity-booster-stats {
    margin: 0.5rem 0 0.25rem;
    padding: 0.4rem 0.65rem;
    background: var(--parchment-dark);
    border-left: 3px solid var(--rule);
    border-radius: 0 4px 4px 0;
}
.activity-booster-stats-heading {
    margin: 0 0 0.3rem;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--ink-light);
}
.activity-booster-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.activity-booster-list li { margin: 0.15rem 0; }
.activity-booster-row {
    /* Block + inline-aligned avatar (not flex) so the name / handle /
       time spans flow as natural inline text. inline-flex laid them out
       as separate columns, and on a narrow viewport (iPhone SE etc.)
       each column wrapped its own text, giving the row a three-stack
       ridge instead of one collapsed line. */
    display: block;
    text-decoration: none;
    color: inherit;
    font-size: 0.9rem;
    line-height: 1.5;
}
.activity-booster-row:hover .activity-booster-name { text-decoration: underline; }
.activity-booster-avatar {
    width: 24px;
    height: 24px;
    vertical-align: middle;
    /* Replaces the gap: 0.4rem of the former inline-flex layout. */
    margin-right: 0.4rem;
}
.activity-booster-name { color: var(--ink); font-weight: 500; }
/* "↻ X delte · <date>" attribution above a boosted post. Sits tight
   against the post it introduces — like a byline, not its own row. */
.activity-boost-by {
    margin: 0 0 0.15rem;
    font-size: 0.85rem;
    line-height: 1.2;
    color: var(--ink-light);
}
/* Drop the thread's own top padding when it sits right under the
   boost line — otherwise the two stack up to an oversized gap. */
.activity-boost-by + .invitation-thread { padding-top: 0; }

/* The post itself as the lead row of its thread. Rendered HTML
   content — distinct from .invitation-message-body (plain text,
   pre-wrap), so it gets normal block-paragraph flow. */
.post-body { margin: 0.1rem 0; }
.post-body > *:last-child { margin-bottom: 0; }
/* Long-post collapse: server wraps the body in .post-body-collapsible
   when the rendered text estimates to more than ~15 visible lines.
   A hidden checkbox before the body + a <label> after it form a
   pure-CSS toggle — no JS. The checkbox stays tab-reachable
   (positioned off-screen, not display:none) so keyboard users can
   activate it with Space. */
.post-body-toggle {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
}
.post-body-collapsible {
    max-height: 41em;
    overflow: hidden;
    position: relative;
}
.post-body-collapsible::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3em;
    background: linear-gradient(to bottom, rgba(235, 227, 204, 0), var(--parchment-dark));
    pointer-events: none;
}
.post-body-toggle-label {
    display: inline-block;
    margin-top: 0.25rem;
    color: var(--ink-light);
    font-size: 0.9em;
    cursor: pointer;
}
.post-body-toggle-label:hover { color: var(--ink); text-decoration: underline; }
.post-body-toggle:focus-visible + .post-body-collapsible + .post-body-toggle-label {
    outline: 2px solid var(--gold-dark);
    outline-offset: 2px;
}
.post-body-toggle-less { display: none; }
.post-body-toggle:checked + .post-body-collapsible { max-height: none; }
.post-body-toggle:checked + .post-body-collapsible::after { display: none; }
.post-body-toggle:checked ~ .post-body-toggle-label .post-body-toggle-more {
    display: none;
}
.post-body-toggle:checked ~ .post-body-toggle-label .post-body-toggle-less {
    display: inline;
}
/* Article headline above the post body — present only on kind=article
   rows. Plain unstyled link so the clickable headline reads as text. */
.post-title {
    margin: 0.1rem 0 0.25rem;
    font-size: 1.25em;
    font-weight: 700;
    line-height: 1.25;
}
.post-title a { text-decoration: none; color: inherit; }
.post-title a:hover { text-decoration: underline; }

/* Embedded event preview — the "og card" half of a commented boost. */
.event-embed-card {
    display: flex;
    gap: 0.75rem;
    align-items: stretch;
    margin: 0.5rem 0;
    border: 1px solid var(--rule);
    border-radius: 4px;
    overflow: hidden;
    text-decoration: none;
    color: var(--ink);
}
.event-embed-card:hover { border-color: var(--gold-dark); }
.event-embed-card-thumb {
    flex: 0 0 5rem;
    background: var(--parchment-dark);
}
.event-embed-card-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.event-embed-card-body {
    padding: 0.5rem 0.65rem;
    min-width: 0;
}
.event-embed-card-title { font-weight: 600; }

/* Link preview card — Open Graph metadata for a hyperlink embedded
   in a post or message. Reuses .event-embed-card; the title and
   description get a `max-height` cap to contain a pathological page,
   but no line-clamp: the older `-webkit-box + overflow:hidden`
   recipe established a new block formatting context on each box,
   which then refused to wrap around the floated thumb (BFCs don't
   overlap floats). `overflow: clip` is the modern equivalent that
   clips without forming a BFC. */
.link-embed-card-desc {
    margin: 0.15rem 0 0;
    font-size: 0.85rem;
    color: var(--ink-light);
    max-height: 12rem;
    overflow: clip;
}
/* Float the thumb so the body text wraps around it instead of
   staying right-pinned with empty space below the image when the
   description grows past the thumb's height. Scoped to the link-
   preview card; event embeds keep their flex layout. The ::after
   clears the float so the card border still wraps the body. */
.link-embed-card { display: block; }
.link-embed-card::after { content: ""; display: block; clear: both; }
.link-embed-card .event-embed-card-thumb {
    float: left;
    width: 5rem;
    height: 5rem;
    margin: 0.5rem 0.75rem 0.25rem 0.5rem;
}
.link-embed-card .meta { margin: 0.15rem 0 0; }
/* Attribution row: bold site name then host, compact one-liner.
   Truncates with ellipsis on overflow. The bold site name is the
   author-provided label; the host (after the middot) is what the
   click actually goes to, so a spoofed site_name vs. host is still
   exposed side-by-side. */
.link-embed-card-attribution {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.link-embed-card-attribution strong { color: var(--ink); }

/* Quoted-embed card — Mastodon / Bluesky post rendered inline as a
   block quote, not a click-through teaser. The full text (up to 500
   chars) sits inside the block; media drops below it at a sensible
   width; a discrete source link at the bottom lets the reader jump to
   replies. No avatar — the author label on the attribution line is
   enough, and a thumb-sized avatar would dominate the row without
   adding context. */
.embed-quote {
    margin: 0.5rem 0;
    padding: 0.4rem 0.65rem 0.5rem 0.75rem;
    border-left: 3px solid var(--rule);
    background: var(--parchment-dark);
    border-radius: 0 4px 4px 0;
    color: var(--ink);
}
.embed-quote-attribution {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--ink-light);
    margin: 0 0 0.25rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* Mastodon / Bluesky quoted embeds reuse the .invitation-message
   two-column layout for their post header. The .embed-quote container
   keeps the left rule + parchment background so the block still reads
   as quoted; the inner .invitation-message zeroes out its own padding
   so the indent comes from .embed-quote alone. */
.embed-quote .invitation-message { padding: 0; background: transparent; }
.embed-quote .invitation-message-meta { margin: 0 0 0.3rem; }
.embed-quote-text {
    margin: 0;
    white-space: pre-wrap;
}
.embed-quote-media {
    margin: 0.5rem 0 0;
    max-width: 100%;
}
.embed-quote-media img {
    display: block;
    max-width: 100%;
    max-height: 24rem;
    height: auto;
    border-radius: 4px;
}
.embed-quote-source {
    display: inline-block;
    margin: 0.4rem 0 0;
    font-size: 0.85rem;
    color: var(--ink-light);
    text-decoration: none;
}
.embed-quote-source:hover { color: var(--gold-dark); text-decoration: underline; }

/* Placeholder shown while link-preview.js fetches a card — a faint
   pulsing box, roughly card height, so the swap-in barely shifts the
   layout. A slot holding a finished card carries no data attribute and
   so gets none of this. */
.link-card-slot[data-link-preview-url] {
    height: 4rem;
    margin: 0.5rem 0;
    border: 1px solid var(--rule);
    border-radius: 4px;
    background: var(--parchment-dark);
    animation: link-card-pulse 1.4s ease-in-out infinite;
}
@keyframes link-card-pulse {
    0%, 100% { opacity: 0.35; }
    50% { opacity: 0.7; }
}

/* Fav / boost controls on an event item — one-click POSTs. Reply is
   the per-message "Svar" inside the shared thread, not here. */
.activity-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0.75rem;
    margin-top: 0.5rem;
}
.activity-control-form { display: inline; margin: 0; }
.activity-on { color: var(--rust); }
.activity-more { margin-top: 1rem; }
.empty-state {
    color: var(--ink-light);
    padding: 1.5rem 0;
}

/* Microblog posts on the timeline: a compose link, section headings,
   and the compose-page form. */
.activity-compose-link { margin: 0.5rem 0 1rem; }
.activity-section-heading { margin-top: 1.5rem; }

/* Image-attachment slots on the post compose forms (Notis + Artikkel).
   Each slot stacks: file picker + × remove, thumbnail preview, alt
   input. The compose JS clones the first slot on "+ Legg til bilde". */
.post-image-area { margin: 0.75rem 0 1rem; }
.post-image-slots {
    list-style: none;
    padding: 0;
    margin: 0 0 0.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}
.post-image-slot {
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 0.5rem;
    background: var(--parchment-dark);
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.post-image-slot-controls {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.post-image-slot-controls > input[type="file"] { flex: 1 1 auto; min-width: 0; }
/* Repaint the native file-picker button across every <input type="file">
   so "Velg fil" matches the rest of the yplev button vocabulary instead
   of the browser-default grey chip. Mirrors `.btn .btn-sm .btn-secondary`
   (transparent + ink border + parchment ink fill on hover). Covers the
   compose image slots, friends-import CSV upload, event image, and
   avatar upload in settings. */
input[type="file"]::file-selector-button {
    margin-right: 0.5rem;
    padding: 0.25rem 0.7rem;
    font-family: inherit;
    font-size: 0.85rem;
    line-height: 1.3;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--ink);
    border-radius: 3px;
    cursor: pointer;
    transition: background 0.1s ease, color 0.1s ease;
}
input[type="file"]::file-selector-button:hover {
    background: var(--ink);
    color: var(--parchment);
}
.post-image-remove {
    flex: 0 0 auto;
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 0.05rem 0.45rem;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    color: var(--ink-light);
}
.post-image-remove:hover { color: var(--rust); border-color: var(--rust); }
.post-image-thumb {
    display: block;
    max-width: 100%;
    max-height: 12rem;
    height: auto;
    border-radius: 4px;
}
.post-image-thumb[hidden] { display: none; }
.post-image-slot input[type="text"] {
    width: 100%;
    box-sizing: border-box;
    font-family: inherit;
    font-size: 1rem;
    padding: 0.55rem 0.75rem;
    color: var(--ink);
    background: var(--parchment);
    border: 1px solid var(--rule);
    border-radius: 3px;
    transition: border-color 0.1s, box-shadow 0.1s;
}
.post-image-slot input[type="text"]:focus {
    outline: none;
    border-color: var(--gold-dark);
    box-shadow: 0 0 0 3px rgba(232, 196, 90, 0.25);
}
/* The Add button inherits all visual styling from `.btn .btn-sm
   .btn-secondary` — same vocabulary the event form uses for
   "Legg til dato". This class is just a hook for JS targeting and a
   margin shim so it sits below the slot list rather than against
   it. */
.post-image-add { margin-top: 0.25rem; }
/* Timeline toolbar — compose dropdown on the left, filter + sort chips
   on the right of the same row. Wraps to two lines when the chip strip
   runs out of width (multi-org accounts, narrow viewports). */
.activity-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 0.5rem 0 1rem;
}
.activity-toolbar > .activity-filter { margin: 0; }
/* Compose dropdown — native <details> with the trigger styled as the
   inline link, and the items revealed below as a floating panel. The
   panel is position:absolute so opening the dropdown doesn't shift
   the right-side chips on the toolbar row. No JS; open/close is
   browser-native. */
.activity-compose-menu { margin: 0; position: relative; }
.activity-compose-menu > summary {
    list-style: none;
    cursor: pointer;
    display: inline-block;
    /* Icon-only trigger — the Greek-cross glyph needs a touch more
       weight than body text to read as a button affordance. */
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1;
    padding: 0.1rem 0.3rem;
}
.activity-compose-menu > summary::-webkit-details-marker { display: none; }
.activity-compose-menu-items {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 0.35rem;
    padding: 0.4rem 0.5rem;
    border: 1px solid var(--rule);
    border-radius: 4px;
    background: var(--parchment-dark);
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    z-index: 10;
    white-space: nowrap;
}
.activity-compose-menu-items a { text-decoration: none; }
.activity-compose-menu-items a:hover { text-decoration: underline; }

/* Images attached to a federated Note. Hot-linked from the origin
   server. Single attachment fills the available width up to a cap;
   2-4 images form a small grid. The figure carries no chrome — the
   image rounds against the card background. */
/* Every visible attachment renders full-width, stacked vertically —
   multi-column mosaics don't read well at any viewport with alt text
   underneath. Timeline rows pass teaserMoreHref so the renderer
   shows only the hero image + a "+ N flere bilder" link; hidden <a>
   elements for the rest stay in the DOM so the lightbox can still
   carousel through the full set on tap. */
.invitation-message-attachments {
    margin: 0.5rem 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.invitation-message-attachment { margin: 0; min-width: 0; }
.invitation-message-attachment-link { display: block; line-height: 0; cursor: zoom-in; }
.invitation-message-attachment img {
    width: 100%;
    max-height: 32rem;
    object-fit: contain;
    background: var(--parchment);
    border-radius: 4px;
    display: block;
}
.invitation-message-attachments-more {
    align-self: flex-start;
    font-size: 0.8rem;
    color: var(--ink-light);
    text-decoration: none;
}
.invitation-message-attachments-more:hover {
    text-decoration: underline;
    color: var(--ink);
}

/* Fullscreen image lightbox — built and injected by lightbox.js on
   first click on an .invitation-message-attachment-link. Hidden by
   default; .open shows it. Click outside the image, the × button,
   or Esc dismisses. Prev / next arrows + arrow keys + swipe navigate
   within the same .invitation-message-attachments group. */
.lightbox {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(0, 0, 0, 0.92);
    align-items: center;
    justify-content: center;
}
.lightbox.open { display: flex; }
.lightbox-stage {
    max-width: min(100vw, 1400px);
    max-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    box-sizing: border-box;
}
.lightbox-stage img {
    max-width: 100%;
    max-height: calc(100vh - 4rem);  /* room for caption + counter */
    object-fit: contain;
    display: block;
}
.lightbox-close,
.lightbox-prev,
.lightbox-next {
    position: absolute;
    background: rgba(0, 0, 0, 0.5);
    border: 1px solid rgba(255, 255, 255, 0.3);
    color: #fff;
    font-size: 1.5rem;
    line-height: 1;
    width: 2.4rem;
    height: 2.4rem;
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}
.lightbox-close { top: 0.75rem; right: 0.75rem; }
.lightbox-prev { left: 0.75rem; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 0.75rem; top: 50%; transform: translateY(-50%); }
.lightbox-close:hover,
.lightbox-prev:hover,
.lightbox-next:hover { background: rgba(0, 0, 0, 0.75); }
.lightbox-counter {
    position: absolute;
    top: 0.75rem;
    left: 50%;
    transform: translateX(-50%);
    color: rgba(255, 255, 255, 0.7);
    font-size: 0.85rem;
    background: rgba(0, 0, 0, 0.4);
    padding: 0.2rem 0.6rem;
    border-radius: 3px;
}
.lightbox-counter:empty { display: none; }
.invitation-message-attachment-caption {
    font-size: 0.8rem;
    color: var(--ink-light);
    margin: 0.2rem 0 0;
}
.compose-form textarea {
    width: 100%;
    box-sizing: border-box;
    font-family: inherit;
    font-size: 1rem;
    padding: 0.6rem;
    border: 1px solid var(--rule);
    border-radius: 3px;
    background: var(--parchment);
    color: var(--ink);
    resize: vertical;
    margin-bottom: 0.5rem;
}
/* "Du skriver om: <event note>" block above the textarea on the
   Notis / Artikkel compose pages. The .compose-about-note container
   wraps the rendered event-Note HTML in a card shape so the author
   sees an explicit "this is what's being attached" preview rather
   than free-floating paragraphs. */
.compose-about { margin-bottom: 0.75rem; }
.compose-about .meta { margin: 0 0 0.3rem; }
.compose-about-note {
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 0.6rem 0.85rem;
    background: var(--parchment-dark);
}
.compose-about-note > :first-child { margin-top: 0; }
.compose-about-note > :last-child { margin-bottom: 0; }
