.addButton {
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    width: 100%;
    padding-bottom: var(--space-md);
}

form {
    display: flex;
    flex-direction: column;
    align-items: center;

    section {
        margin: var(--space-md);
        width: 100%;
    }
}

input:not([type="file"]):not([type="radio"]),
textarea,
label.htmlEditor div.editorHolder,
label.htmlEditor div.htmlEditor-readonly-value,
div.binary-file,
div.binary-attr-image,
select {
    padding: var(--space-md) var(--space-ms) var(--space-ms) var(--space-ms);
    border-style: solid;
    border-width: 1px;
    border-radius: var(--radius-md);
}

/*
 * Field-level validation errors.
 * Atoms render `class="error"` on the input + a sibling `.field-error` span
 * when the controller passes a non-empty error string. The error coloring
 * (border/outline/text shifts) lives in colors.css; only structure stays
 * here. `.icon-picker-container` has no default border — use outline to
 * avoid layout shift between error/non-error states. The
 * zone-picker error state is carried by the inner `.zone-typeahead-input`;
 * the wrapper-level class on `.zone-picker` is reserved for future
 * structural decoration if needed.
 */
.icon-picker-container.error {
    outline-width: 1px;
    outline-style: solid;
    outline-offset: var(--space-xs);
    border-radius: var(--radius-md);
}

.field-error {
    display: block;
    margin-top: var(--space-xs);
    font-size: var(--font-size-xs);
    line-height: var(--typography-lineHeightCompact);
}
div.binary-attr-image {
    width: 12rem;
    margin-top: var(--space-ms);
    img {
        width: 12rem;
        height: auto;
        border-radius: var(--radius-sm);
    }
}

span.clear-file {
    input {
        margin-top: var(--space-xs);
        margin-right: var(--space-xs);
    }

    span {
        position: relative;
        top: -0.5rem;
    }
}
input[type=checkbox] {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;

    & {
        height: 30px;
        padding: 0;
        width: 50px;
        border-radius: 15px;
        display: inline-block;
        position: relative;
        margin: 0;
    }

    &:after,
    &:checked:after {
        font-size: xx-small;
        line-height: 1.5rem;
        text-align: center;
    }

    &:after {
        content: "No";
        position: absolute;
        top: 2px;
        left: 2px;
        width: 24px;
        height: 24px;
        border-radius: 50%;
        box-shadow: var(--shadow-fine);
        transition: var(--action-transition);
    }

    &:checked:after {
        content: "Yes";
        transform: translatex(20px);
    }
}

label {
    width: 100%;

    margin: var(--space-md);

    span.label-text {
        margin-left: .35rem;
        position: relative;
        top: 0.625rem;
        border-style: solid;
        border-width: 1px;
        border-radius: var(--radius-md);
        padding: 0 0.4rem;

        &:has(+ select) {
            top: 0.125rem;
        }

        /* When the chip is directly followed by a .zone-typeahead, both the
           chip and the wrapper are position:relative with z-index:auto, so
           stacking falls back to document order: the wrapper, coming later,
           paints on top of the chip's bottom half (the chevron's anchor
           positioning is why the wrapper is positioned in the first place).
           Lift the chip above its positioned sibling. Scoped narrowly so
           static-positioned siblings keep their existing default-on-top
           paint order.

           Same issue applies to the entity-chooser field wrapper, which
           is position:relative so the tile-view preview popover can
           anchor itself, and to the .json-editor box,
           which is position:relative so its highlight overlay and Format
           button can be absolutely positioned — its top border otherwise
           paints through the label chip. */
        &:has(+ .zone-typeahead),
        &:has(+ .entity-chooser-field),
        &:has(+ .json-editor),
        &:has(+ .string-typeahead-box) {
            z-index: 1;
        }
    }

    /* One box model for every field atom: border-box at the label's full
       width. Replaces two opposite content-box compensation hacks
       (inputs at 100% − padding, selects at 100% + 0.1rem) that left
       selects bleeding past the shared field boundary. */
    input,
    textarea,
    select {
        box-sizing: border-box;
        width: 100%;
    }

    div.help-text {
        margin-left: var(--space-md);
    }

    & > input[type=checkbox] {
        vertical-align: middle;
    }

    input[type=checkbox] + span.label-text {
        position: unset;
    }
}

fieldset {
    /* Vertical rhythm only: the border sits on the shared field boundary
       (a horizontal margin indented fieldsets 1rem past every other field). */
    margin: var(--space-md) 0;
    padding: var(--space-ms) var(--space-md) var(--space-md) var(--space-md);
    border-style: solid;
    border-width: 1px;
    border-radius: var(--radius-md);

    legend {
        font-size: var(--font-size-sm);
        padding: 0 0.4rem;
        border-style: solid;
        border-width: 1px;
        border-radius: var(--radius-md);
    }

    label {
        display: block;
        margin: var(--space-sm) 0;
        width: auto;
    }

    &.toggle {
        border: none;
        padding: 0;
        margin: 0;

        legend {
            border: none;
            padding: 0;
        }

        .selected {
            font-weight: var(--typography-fontWeightMedium);
        }
    }
}

/* CSS Editor styling */
.css-editor-label {
    display: flex;
    flex-direction: column;
    flex: 1;
}

/* These editors wrap the field in a flex-column <label>, which blockifies the
   `span.label-text` chip and stretches it to the full container width (default
   align-items: stretch). A full-width chip paints its opaque background over
   the WHOLE top border of the field below it, erasing the border instead of
   notching it. Shrink the chip back to its content size so the border shows on
   either side, matching the standard outlined text field. */
.css-editor-label > span.label-text,
.json-editor-label > span.label-text {
    align-self: flex-start;
}

.css-editor {
    flex: 1;
    min-height: 400px;
    font-family: var(--typography-font-family-fixed);
    white-space: pre;
    tab-size: 2;
    line-height: var(--typography-lineHeightCompact);
}

/*
 * JSON Editor. Textarea-over-<pre> overlay pattern: the
 * textarea is transparent and shows the user's caret; the <pre> behind it
 * carries the syntax-highlighted text. Both elements MUST share identical
 * font, line-height, padding, and border to keep characters aligned.
 */
.json-editor-label {
    display: flex;
    flex-direction: column;
    flex: 1;
    width: 100%;
}

.json-editor {
    position: relative;
    flex: 1;
    width: 100%;
    overflow: hidden;
    border-radius: var(--radius-md);
    border-style: solid;
    border-width: 1px;
}

.json-editor__highlight,
.json-editor__input {
    margin: 0;
    padding: var(--space-md) var(--space-ms) var(--space-ms) var(--space-ms);
    border: 0;
    font-family: var(--typography-font-family-fixed);
    font-size: 1rem;
    line-height: var(--typography-lineHeightCompact);
    tab-size: 2;
    white-space: pre;
    overflow-wrap: normal;
    overflow: auto;
    box-sizing: border-box;
}

.json-editor__highlight {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: transparent;
}

.json-editor__highlight code {
    display: block;
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
    white-space: inherit;
    min-height: 1.4em;
}

.json-editor__input {
    position: relative;
    width: 100%;
    min-height: 20rem;
    /* color + bg + -webkit-text-fill-color must stay transparent across
       :hover / :focus / :active. The global "action" rule on textarea
       uses CSS nesting (&:focus, &:hover) which beats a plain class
       selector on specificity, so the transparency is forced with
       !important — load-bearing for the overlay pattern (textarea on
       top of highlighted <pre>; if the textarea paints text or bg,
       the overlay is hidden). Caret + ::selection coloring lives in
       colors.css with the highlight tokens. */
    background: transparent !important;
    color: transparent !important;
    -webkit-text-fill-color: transparent !important;
    box-shadow: none !important;
    resize: vertical;
    outline: none;
}

.json-editor__format {
    position: absolute;
    top: var(--space-xs);
    right: var(--space-xs);
    z-index: 2;
    padding: var(--space-xs) var(--space-sm);
    font-size: var(--font-size-xs);
    cursor: pointer;
    border-style: solid;
    border-width: 1px;
    border-radius: var(--radius-sm);
    line-height: 1;
}

.json-editor__format:focus {
    outline-width: 2px;
    outline-style: solid;
    outline-offset: 1px;
}

/* Icon Picker Styles */
.icon-picker-label {
    display: flex;
    flex-direction: column;
    /* flex makes this label block-level, so the global label margin becomes
       a real 1rem offset off the shared field boundary — zero it. */
    margin-left: 0;
    margin-right: 0;
}

.icon-picker-container {
    display: flex;
    align-items: center;
    gap: var(--space-ms);
    margin-top: var(--space-sm);
}

.icon-picker-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    border-radius: var(--radius-md);
    border-width: 1px;
    border-style: solid;
    font-size: 1.5rem;
}

.icon-picker-preview .no-icon {
    font-size: var(--font-size-xs);
}

/* Icon Picker Modal (scrim coloring in colors.css) */
.icon-picker-modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}

/* The content box is a system .modal (chrome from components.css); this
   layer adds only the explicit width a non-<dialog> flex child needs and
   the column layout the grid/footer rely on. */
.icon-picker-modal-content {
    width: min(640px, 92vw);
    max-height: 80vh;
    display: flex;
    flex-direction: column;
}

/* System icon-button; only the oversized glyph remains. */
.icon-picker-modal-close {
    font-size: 1.5rem;
}

.icon-picker-controls {
    display: flex;
    gap: var(--space-ms);
    padding: var(--space-md);
    border-bottom-width: 1px;
    border-bottom-style: solid;
}

.icon-picker-search {
    flex: 1;
}

/* Inputs ride the global field recipe + action coloring; only sizing here. */
.icon-picker-search input {
    width: 100%;
    padding: var(--space-ms);
    font-size: var(--font-size-base);
    box-sizing: border-box;
}

.icon-picker-style select {
    padding: var(--space-ms);
    font-size: var(--font-size-base);
    cursor: pointer;
}

.icon-picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
    gap: var(--space-sm);
    padding: var(--space-md);
    overflow-y: auto;
    flex: 1;
}

/* The 1px border slot is reserved here so the hover border (colors.css)
   doesn't shift layout; the transparent base color also lives there. */
.icon-picker-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--space-ms) var(--space-sm);
    border-width: 1px;
    border-style: solid;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: var(--action-transition);
}

.icon-picker-item i {
    font-size: 1.5rem;
    margin-bottom: var(--space-xs);
}

.icon-picker-item span {
    font-size: 0.6rem;
    text-align: center;
    word-break: break-word;
}

.icon-picker-more,
.icon-picker-no-results {
    grid-column: 1 / -1;
    text-align: center;
    padding: var(--space-md);
    font-size: var(--font-size-sm);
}

/* ============= TEMPORAL ATOMS =============
   Date/time/datetime inputs paired with an IANA zone picker. Two concerns:
   (1) the native picker indicator (calendar / clock icon) must respect dark
   mode — `color-scheme: light dark` lets browsers pick the dark variant of
   the user-agent control colours; (2) the (input + zone select) pair should
   render side-by-side on desktop since neither is wide and they belong
   together, then stack on narrow viewports. */

.input-date,
.input-datetime,
.input-date-zoned,
.input-time-zoned,
.input-datetime-zoned,
.input-timestamp,
input[type="date"],
input[type="time"],
input[type="datetime-local"] {
    color-scheme: light dark;
}

.zoned-temporal {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}

/* Override the inherited 100%-width rules from the parent `label` block —
   without this, both children consume the row and flex-wrap stacks them.
   Equal flex-grow + flex-basis: 0 makes the two children split the row 50/50;
   the min-widths prevent collapse on narrower viewports until the @media
   breakpoint kicks in and stacks them. */
.zoned-temporal > input[type="date"],
.zoned-temporal > input[type="time"],
.zoned-temporal > input[type="datetime-local"] {
    width: auto;
    flex: 1 1 0;
    min-width: 10rem;
}

.zoned-temporal > .zone-picker {
    width: auto;
    flex: 1 1 0;
    min-width: 12rem;
}

/* ============= ZONE PICKER COMBOBOX =============
   Combobox/typeahead replacement for the native <select>. Wrapper carries
   class="zone-picker zone-typeahead" so the temporal-atom layout above
   keeps targeting it via .zone-picker. Markup is rendered by the
   zonePicker macro; behaviour is in warhol-apps.js. */

.zone-typeahead {
    position: relative;
}

/* Chevron — visual cue that this is a dropdown control, replacing the
   native <select> caret we lost when switching to <input>. Positioned
   absolutely on the wrapper; pointer-events: none so clicks fall through
   to the input. Coloring exemption: this is a border-triangle hack — the
   transparent sides ARE the shape and the `border:` shorthand would reset
   a relocated border-top-color (this file loads after colors.css), so the
   visible-edge color stays here with its structure. */
.zone-typeahead::after {
    content: "";
    position: absolute;
    right: 0.85rem;
    top: 50%;
    transform: translateY(-25%);
    pointer-events: none;
    border: 0.3rem solid transparent;
    border-top-color: var(--text-secondary);
}

.zone-typeahead > input.zone-typeahead-input {
    width: 100%;
    box-sizing: border-box;
    padding-right: var(--space-xl); /* leave room for the chevron */
}

.zone-typeahead-data {
    display: none;
}

.zone-typeahead-suggestions {
    position: absolute;
    z-index: 20;
    left: 0;
    right: 0;
    top: 100%;
    margin: 2px 0 0 0;
    padding: 0;
    list-style: none;
    max-height: 18rem;
    overflow-y: auto;
    border-width: 1px;
    border-style: solid;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-4);
}

.zone-typeahead-section-label {
    padding: 0.4rem var(--space-ms) 0.2rem var(--space-ms);
    font-size: var(--font-size-xs);
    font-weight: var(--typography-fontWeightSemiBold);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    pointer-events: none;
}

.zone-typeahead-option {
    display: block;
    width: 100%;
    padding: 0.4rem var(--space-ms);
    border: none;
    border-radius: 0;
    text-align: left;
    cursor: pointer;
    font: inherit;
}

/* ============= TIMESTAMP EDIT MODE =============
   TIMESTAMP attributes store a UTC Instant (not a wall-clock + zone).
   The zone picker on a timestamp row is a *view* choice only — the
   stored value is the UTC instant shown in the meta caption below.
   Visual distinction (left-border accent + UTC caption + explainer)
   makes this clear vs. DATETIME_ZONED where the zone is part of the
   stored value. */

.timestamp-edit {
    border-left-width: 3px;
    border-left-style: solid;
    padding-left: var(--space-ms);
    margin-left: -var(--space-ms);
}

.timestamp-meta {
    margin-top: 0.4rem;
    font-size: var(--font-size-xs);
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.timestamp-utc-caption code {
    font-family: var(--typography-font-family-fixed), monospace;
    font-size: var(--font-size-xs);
    padding: 0.05rem 0.35rem;
    border-radius: var(--radius-sm);
}

.timestamp-explain {
    font-style: italic;
    opacity: 0.85;
}

/* Inspect-mode display — same UTC caption pattern, no border. */
.timestamp-inspect {
    display: inline-flex;
    flex-direction: column;
    gap: 0.15rem;
}

.timestamp-inspect .timestamp-wallclock small {
    margin-left: 0.35rem;
}

.timestamp-inspect .timestamp-utc-caption {
    font-size: var(--font-size-xs);
}

@media (max-width: 480px) {
    .zoned-temporal {
        flex-direction: column;
        align-items: stretch;
    }
    .zoned-temporal > input[type="date"],
    .zoned-temporal > input[type="time"],
    .zoned-temporal > input[type="datetime-local"],
    .zoned-temporal > .zone-picker {
        width: 100%;
        flex: 0 0 auto;
    }
}

/* ============= FILE INPUT (BINARY ATTRIBUTES) =============
   Style the native ::file-selector-button so it matches Warhol's
   button look (existing button/.button rules live in components.css)
   instead of the unstyled UA-default control. */

input[type="file"]::file-selector-button {
    display: inline-block;
    margin: 0 var(--space-sm) 0 0;
    padding-block: var(--space-xs);
    padding-inline: 12px;
    border-width: thin;
    border-style: outset;
    border-radius: var(--radius-md);
    font: inherit;
    cursor: pointer;
}

/*
 * Unsaved-changes indicator pill.
 *
 * Injected by the dirty-tracker block in warhol-apps.js next to the submit
 * button of any form with a save/create-style submit. The pill is a sibling
 * of the submit button so it inherits the form-actions row layout.
 *
 * State is driven by data-state on the .dirty-indicator element:
 *   - clean (default): hidden (visibility: hidden + opacity: 0).
 *   - dirty:           visible, warning-coloured ("Unsaved changes").
 *   - saved:           visible, success-coloured ("Saved"), fades after ~2s.
 */

/* Status pill — THE inline save/state feedback pill. Composing widgets add
   placement and their own visibility machinery; state colors below are
   shared by both consumers. */
.status-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 0.2rem 0.7rem;
    border-radius: var(--radius-pill);
    border-width: 1px;
    border-style: solid;
    font-size: var(--font-size-sm);
    line-height: var(--typography-lineHeightCompact);
    vertical-align: middle;
    transition: color 200ms ease, border-color 200ms ease,
                background-color 200ms ease;
}

.dirty-indicator {
    margin-inline-start: 0.6rem;
    visibility: hidden;
    opacity: 0;
    transition: opacity 200ms ease, visibility 0s linear 200ms,
                color 200ms ease, border-color 200ms ease,
                background-color 200ms ease;
}

.dirty-indicator[data-state="dirty"],
.dirty-indicator[data-state="saved"] {
    visibility: visible;
    opacity: 1;
    transition: opacity 200ms ease, visibility 0s linear 0s,
                color 200ms ease, border-color 200ms ease,
                background-color 200ms ease;
}

/* Per-state warning/success/error pill coloring lives in colors.css. */

.dirty-indicator-icon {
    font-size: 0.95em;
}

/*
 * Promote the submit button when the form is dirty: drop the muted
 * "secondary" treatment in favour of the primary action style. Scoped to the
 * [data-dirty="true"] toggle that the tracker sets, so we don't have to touch
 * the crudForm macro. The primary coloring lives in colors.css.
 */
button[type="submit"][data-dirty="true"],
input[type="submit"][data-dirty="true"] {
    font-weight: var(--typography-fontWeightSemiBold);
    transition: background-color 200ms ease, font-weight 0s;
}

/*
 * Draft email compose pane. The pane mirrors the
 * read-an-email shell (.message-view + toolbar + header + body), with a
 * couple of additions: an inline save-status pill in the toolbar, an
 * actions row at the bottom for Send / Delete, and an unstyled subject
 * input that visually replaces the email's <h2> subject.
 */

.draft-compose form {
    /* Override forms.css form{display:flex;flex-direction:column;align-items:center}
       so the message-view layout matches the read-an-email pane. */
    display: block;
    width: 100%;
}

.draft-save-status {
    margin-inline-start: auto;
}

.message-view-actions {
    display: flex;
    gap: 0.6rem;
    margin: var(--space-md) 0;
}

.message-view .message-view-subject input[name="subject"] {
    /* width: 100% must measure the border box: on the content box the padding and
       border land outside the header and the compose pane scrolls sideways. */
    box-sizing: border-box;
    width: 100%;
    font-size: 1.5rem;
    font-weight: var(--typography-fontWeightSemiBold);
    padding: 0.4rem var(--space-sm);
    border-width: 1px;
    border-style: dashed;
    border-radius: var(--radius-sm);
}

/* Focus flips the dashed affordance solid; border-color in colors.css. */
.message-view .message-view-subject input[name="subject"]:focus {
    outline: none;
    border-style: solid;
}

/*
 * Compose-pane body sizing + chrome trimming.
 * .message-view-body has no fixed background, no inner padding around the
 * label-wrapped editor; the editor itself gets a generous min-height so
 * the user has real writing room before the page starts scrolling.
 */
.draft-compose .message-view-body {
    padding: 0;
}

.draft-compose .message-view-body label.htmlEditor {
    margin: 0;
    width: 100%;
}

.draft-compose .message-view-body label.htmlEditor span.label-text,
.draft-compose .message-view-body label.htmlEditor div.help-text {
    display: none;
}

.draft-compose .message-view-body label.htmlEditor div.editorHolder {
    min-height: 22rem;
    /* Trim the 1rem 0.75rem 0.75rem 0.75rem default from forms.css — text
       still has horizontal breathing room but no vertical dead space. */
    padding: 0.35rem var(--space-ms);
}

/* Drop the 0.6rem top margin .rte-toolbar carries elsewhere — in the
   compose pane the toolbar sits flush under the subject row. */
.draft-compose .message-view-body label.htmlEditor .rte-toolbar {
    margin: 0;
}

/* ============= PUBLIC ADDRESS =============
   The optional slug on library content and media. */
.slug-fieldset {
    border: none;
    padding: 0;
    margin: var(--space-sm) 0;
    width: 100%;
}

.slug-fieldset > legend {
    border: none;
    padding: 0;
    font-weight: var(--typography-fontWeightBlack);
}

.slug-fieldset .optional-flag {
    font-weight: var(--typography-fontWeightRegular);
}

/* Screen-reader-only utility — used by drafts/compose.ftlh's subject label. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ===== String Type-Ahead editor + tag chips =====
   Layout only — coloring lives in colors.css. The control matches the standard
   outlined text field: the global `label span.label-text` chip rides the top
   border of `.string-typeahead-box` (which carries the same border/radius/
   top-padding as an <input>); the input and chips live inside. The box owns
   the outline, so the inner input is seamless (no box-in-a-box). */
.string-typeahead-box {
    /* Match the standard `label input` box model exactly: content-box with the
       same width / padding / border, so the box (and the suggestion panel that
       spans it) lines up with sibling fields. border-box made it ~26px short. */
    box-sizing: content-box;
    width: calc(100% - 1.5rem);
    padding: var(--space-md) var(--space-ms) var(--space-ms) var(--space-ms);
    border-style: solid;
    border-width: 1px;
    border-radius: var(--radius-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    position: relative; /* anchor for the suggestion panel */
}
/* Seamless inner input — the box draws the border / background. */
.string-typeahead-box .string-typeahead-input {
    width: 100%;
    margin: 0;
    padding: 0.15rem 0.1rem;
    border: 0;
    border-radius: 0;
}
.string-typeahead-box .string-typeahead-input:focus {
    outline: none;
}
/* The outline: none above had no replacement — the box (not the
   seamless inner input) draws the ring, matching how it draws the border. */
.string-typeahead-box:has(.string-typeahead-input:focus-visible) {
    outline-width: 2px;
    outline-style: solid;
    outline-offset: 2px;
}
.string-typeahead-chips,
.chip-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
}
/* Collapse the chip row when empty so an input-only box has no dead space. */
.string-typeahead-chips:empty {
    display: none;
}
.chip-list-label {
    font-weight: var(--typography-fontWeightSemiBold);
    margin-right: var(--space-xs);
}
/* Tag values are system chips (span.chip chip-sm — static, with .chip-remove
   as the only affordance). Plain variant (showAsChip=false): strip the pill
   chrome so the values read as plain multi-valued text rather than chips. */
.string-typeahead--plain .chip {
    padding: 0;
    border-radius: 0;
    border: 0;
}

/* Guided combobox suggestion panel — layout only; coloring
   lives in colors.css. Absolutely positioned below the box, full width. */
.string-typeahead-panel {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + var(--space-xs));
    z-index: 30;
    border-style: solid;
    border-width: 1px;
    border-radius: var(--radius-md);
    overflow: hidden;
}
.string-typeahead-panel[hidden] {
    display: none;
}
.string-typeahead-panel-head {
    padding: var(--space-sm) 0.7rem 0.3rem;
    font-size: var(--font-size-xs);
    font-weight: var(--typography-fontWeightSemiBold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.string-typeahead-options {
    max-height: 16rem;
    overflow-y: auto;
    padding-bottom: var(--space-xs);
}
.string-typeahead-option {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 0.4rem 0.7rem;
    cursor: pointer;
    font-size: var(--font-size-sm);
    line-height: var(--typography-lineHeightCompact);
}
.string-typeahead-tick {
    flex: 0 0 1rem;
    text-align: center;
}
.string-typeahead-option-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.string-typeahead-count {
    flex: 0 0 auto;
    min-width: 1.75rem;
    text-align: center;
    padding: 0 0.45rem;
    border-radius: var(--radius-full);
    font-size: var(--font-size-xs);
    font-variant-numeric: tabular-nums;
}
.string-typeahead-new {
    flex: 0 0 auto;
    padding: 0.05rem 0.4rem;
    border-radius: var(--radius-full);
    font-size: var(--font-size-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.string-typeahead-empty {
    padding: var(--space-sm) 0.7rem;
    font-size: var(--font-size-sm);
}

/*
 * Content-aware form width
 * ========================================
 * Opt-in modifiers on the <form> (or a section body). The global `form` rule is
 * untouched, so unmigrated forms render exactly as before. Three layouts:
 *   .form-narrow  single comfortable column, left-anchored
 *   .form-grid    12-col content-aware grid; fields carry an fld-* width class
 *   .form-sheet   stack of always-open .form-section cards (head | grid body)
 *
 * Field width vocabulary (the fld-* classes, emitted by the kdf atoms via size=):
 *   sm  = span 3  — codes, counts, years, short enums
 *   md  = span 6  — names, slugs, emails, phones (the STRING default)
 *   lg  = span 8  — titles, headlines, URLs
 *   full= span 12 — descriptions, rich text, editors (also the unsized default)
 * A custom editor opts into the system by putting an fld-* class on its field
 * root; anything inside .form-grid that is not sized falls back to full row.
 */
.form-narrow {
    max-width: var(--form-measure);
    width: 100%;
    align-self: stretch;
    box-sizing: border-box;
}

.form-grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--space-md);
    row-gap: var(--space-xs);
    align-items: start;
    /* Half-fluid: fill the canvas up to the base width, then grow at --form-growth
       of the extra space (the rest stays as margin), capped at --form-measure-max.
       At growth=0 this is a hard cap at the base; at 1 it is fully fluid. */
    width: var(--content-measure);
    max-width: var(--form-measure-max);
    box-sizing: border-box;
}
/* Any direct child spans the full row by default (intro text, buttons, the
   profile-picture block, a rendered attribute editor); fld-* classes narrow it. */
.form-grid > * {
    grid-column: 1 / -1;
}
/* The global `form section` rule (width:100% + 1rem side margins, content-box)
   overflows the grid track by padding+margin — bit the attribute form's in-use
   alert. Sections inside a grid are contained. */
.form-grid > section {
    box-sizing: border-box;
    width: 100%;
    margin: var(--space-sm) 0;
}
.form-grid > label,
.form-grid > fieldset {
    margin: var(--space-sm) 0;
}
/* Every span is a multiple of 3 so all field edges snap to quarter rails
   (25/50/75/100%) regardless of how sizes mix — mixed rows otherwise produce
   near-miss right edges (a span-8 next to a 3+6 row was off by one column).
   Any future size must keep this invariant. */
.form-grid > .fld-sm { grid-column: span 3; }
.form-grid > .fld-md { grid-column: span 6; }
.form-grid > .fld-lg { grid-column: span 9; }
.form-grid > .fld-full { grid-column: 1 / -1; }

.form-sheet {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-md);
    /* Half-fluid — see .form-grid above. */
    width: var(--content-measure);
    max-width: var(--form-measure-max);
}
.form-sheet .form-section {
    display: grid;
    grid-template-columns: 15rem 1fr;
    gap: var(--space-lg) var(--space-xl);
    width: 100%;
    margin: 0;
    padding: var(--space-lg);
    border-width: 1px;
    border-style: solid;
    border-radius: var(--radius-lg);
    /* width:100% + padding must not overflow the parent — no global border-box reset. */
    box-sizing: border-box;
}
/* Headless card: no left head-rail, the grid body spans the whole card. Used by
   the CMS form's ungrouped / direct-attribute runs, which have no section name
   (named sections come from attribute GROUPS, rendered as accordions). */
.form-sheet .form-section--plain {
    grid-template-columns: 1fr;
}
.form-sheet .form-section-head h3 {
    margin: 0 0 var(--space-xs);
    font-size: var(--font-size-lg);
    font-weight: var(--typography-fontWeightSemiBold);
    line-height: var(--typography-lineHeightCompact);
}
.form-sheet .form-section-head .help-text {
    margin: 0;
}
.form-sheet .form-section-body {
    margin: 0;
    /* The card body fills its column; the half-fluid cap belongs to the sheet, not
       to a grid nested inside a card (which would leave a gap on wide forms). */
    width: 100%;
    max-width: none;
}
.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    /* Override the global `form section` horizontal margin (it would push a
       width:100% section past the canvas edge). */
    margin: var(--space-md) 0 0;
    box-sizing: border-box;
}

/* Collapse to a single column when the canvas is narrow OR the CMS chooser is
   docked (the canvas loses ~420px, so the multi-column grid would crush). */
@media (max-width: 900px) {
    .form-grid > .fld-sm,
    .form-grid > .fld-md,
    .form-grid > .fld-lg,
    .form-grid > .fld-full { grid-column: 1 / -1; }
    .form-sheet .form-section { grid-template-columns: 1fr; gap: var(--space-md); }
}
body[data-chooser-open] .form-grid > .fld-sm,
body[data-chooser-open] .form-grid > .fld-md,
body[data-chooser-open] .form-grid > .fld-lg,
body[data-chooser-open] .form-grid > .fld-full { grid-column: 1 / -1; }
body[data-chooser-open] .form-sheet .form-section { grid-template-columns: 1fr; }