* feat(activity): image tagging jobs for bulk Add tags and Auto-tag
POST /prompt_manager/jobs/image-tags starts one record-changing
"image-tags" job over 1..500 listed images (202, or the same 409 busy
envelope as /jobs/scan). Add tags hashes each current file and unions a
fixed set into that byte record; Auto-tag captures each still image, asks
one legacy engine for its top N suggestions on the capture (count from
the options or autotag.defaults.suggestion_limit), re-hashes the file and
unions only onto the same bytes. Outcomes are grouped as tagged,
unchanged, skipped (video, too large, file changed, file not found, not a
still image) and errors; cancel stops at a file boundary.
Auto-tag holds the shared model gate for the whole job (409 model-busy at
start when it is held), loads the model once and never downloads one: an
unavailable model fails the job before the first file. The registry gains
blocker() so a start answers busy before taking the gate.
* feat(annotations): Add tags and Auto-tag selected in the selection bar
The Images selection bar gains the concept's Add tags and, beside it,
Auto-tag selected (D85). Add tags starts the image-tags Activity job with
the dialog's comma-separated names; Auto-tag selected reuses the Tag image
model panel (server defaults, Suggestions count) without Generate, refuses
a model that is not downloaded, and either starts the job (Apply
automatically) or walks the selection one Tag image dialog at a time
(Review each image, D90). Busy, model-busy and refused starts toast why;
the selection stays; a finished job's files are read again.
* feat(activity): show image tagging jobs with their own outcomes
Image tagging jobs get their own Activity view (D89): the pill reads
"Tagging", the item and heading line name the images and end with
"N tagged · M errors", and the job view shows Tagged, Unchanged, Skipped
and Errors tiles and outcomes, opens Tagged and Unchanged rows in the
viewer, and reads "Tagging log" and "Cancel tagging". The per-kind table
lives in activity-store.mjs; the scan's view is unchanged.
The selection bar's copy without a concept line is recorded as D117.
* fix(annotations): final review round for bulk image tagging
- Rename the D117 copy deviation to D118 (D117 belongs to another PR).
- The tagging job's progress line gives the scan's "about X min left"
estimate, with each finished file as one batch.
- The start body cap fits 500 targets of the longest legal path, even
escaped; one byte over answers 413.
- A capture store at capacity is retried with a short backoff, then the
file is skipped with a rerun sentence instead of a Tag-dialog error.
- Output roots and their labels are resolved once per job in the
executor, not twice per file on the event loop.
- Review each image holds Stop reviewing, Escape and x while a save is in
flight, and its primary reads "Save and next" when only the manual
field would be saved.
- An Add tags field that parses to no names keeps the dialog and its
text and reports the message on the field.
- Image tagging finish toasts carry the counts ("... finished · N tagged
· M errors").
- Specs: the bulk spec waits for the answered Start before checking that
the invalid press sent nothing; the Activity spec uses a real error row.
385 lines
35 KiB
CSS
385 lines
35 KiB
CSS
/* Production overlay on the verbatim concept base (concept.css, D1 of #249). Only production-only states and recorded
|
||
deviations live here; a rule on a selector the concept also styles carries its decision marker. */
|
||
|
||
/* Production-only shell states. */
|
||
main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||
/* The page title takes focus after a save or route change (concept app.js 1168); a keyboard user sees where it went. */
|
||
#page-title:focus { outline: none; }
|
||
#page-title:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||
/* D28: the sidebar shows no connection state, so its dot is neutral instead of the concept's demo green. */
|
||
.connection i { background: var(--ca-muted); }
|
||
/* D31: bounded rendering reads the grid's real width, so at extreme zoom the one-column shell must not grow to the
|
||
sidebar's min-content (the concept's `1fr`); the column is capped at the viewport as production's shell was. */
|
||
@media (max-width: 950px) {
|
||
.app-shell { grid-template-columns: minmax(0, 1fr); /* D31 */ }
|
||
}
|
||
@media (forced-colors: active) {
|
||
nav a[aria-current="page"] { border: 2px solid Highlight; color: LinkText; }
|
||
}
|
||
|
||
/* D53: the running job's progress pill before the Activity button (the approved Activity mockup's `.pill`), in the
|
||
concept's tokens. At <=700px it keeps its verb and percentage; the file counts and bar stay in its accessible name. */
|
||
.activity-pill:not([hidden]) { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--line);
|
||
border-radius: 999px; font-size: 12px; white-space: nowrap; color: var(--ca-text); background: var(--ca-surface); /* D53 */ }
|
||
.activity-pill:hover { border-color: var(--ca-accent); /* D53 */ }
|
||
.activity-pill-bar { display: block; inline-size: 70px; block-size: 5px; border-radius: 3px; background: var(--line); overflow: hidden; /* D53 */ }
|
||
.activity-pill-bar i { display: block; block-size: 100%; inline-size: 0; background: var(--ca-accent); /* D53 */ }
|
||
@media (pointer: coarse) { .activity-pill:not([hidden]) { min-block-size: 40px; /* D53 */ } }
|
||
@media (max-width: 700px) {
|
||
/* D52: with the Activity button the full-width row can be wider than a zoomed phone, so its items wrap. */
|
||
.top-actions { flex-wrap: wrap; /* D52 */ }
|
||
.activity-pill:not([hidden]) { gap: 0; padding-inline: 10px; /* D53 */ }
|
||
.activity-pill-files, .activity-pill-bar { display: none; /* D53 */ }
|
||
}
|
||
|
||
/* D56: each Activity item's title links its job's view in the title's own colour. */
|
||
.activity-item h3 a { color: inherit; text-decoration: none; /* D56 */ }
|
||
.activity-item h3 a:hover { text-decoration: underline; /* D56 */ }
|
||
|
||
/* D56: one job's view, the approved Activity mockup (activity-scan.html) in the concept's tokens: the counter tiles,
|
||
the outcome list beside the selected outcome's files, and the collapsible log. */
|
||
.stat-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; margin: 16px 0; /* D56 */ }
|
||
.stat-row .stat { border: 1px solid var(--line); border-radius: 7px; padding: 12px 14px; background: var(--ca-surface); min-inline-size: 0; /* D56 */ }
|
||
.stat-row .stat dt { font-size: 11px; color: var(--ca-muted); /* D56 */ }
|
||
.stat-row .stat dd { margin: 6px 0 0; font-size: 20px; font-weight: 600; /* D56 */ }
|
||
.stat-row .stat.added dd { color: var(--green); /* D56 */ }
|
||
.stat-row .stat.warn dd { color: var(--warning); /* D56 */ }
|
||
.stat-row .stat.bad dd { color: var(--red); /* D56 */ }
|
||
.surface > .panel-heading > .search-field { max-width: 320px; /* D56 */ }
|
||
.outcomes { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 16px; margin-top: 6px; /* D56 */ }
|
||
.ca-tools .outcome-list button { display: flex; justify-content: space-between; width: 100%; margin-bottom: 6px; text-align: left; /* D56 */ }
|
||
.ca-tools .outcome-list button[aria-pressed="true"] { border-color: var(--ca-accent); background: var(--soft-blue); /* D56 */ }
|
||
.outcome-files .file-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 9px 4px;
|
||
border-bottom: 1px solid var(--line); font-size: 12px; /* D56 */ }
|
||
.ca-tools button.file-row { width: 100%; min-height: 0; justify-content: normal; align-items: start; border: 0;
|
||
border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: inherit; text-align: left;
|
||
white-space: normal; font: inherit; font-size: 12px; /* D56 */ }
|
||
.ca-tools button.file-row:hover:not(:disabled) { background: var(--ca-hover); /* D56 */ }
|
||
.file-row .path { font-family: ui-monospace, monospace; overflow-wrap: anywhere; /* D56 */ }
|
||
.file-row .why { color: var(--ca-muted); /* D56 */ }
|
||
.outcome-files > p.small { color: var(--ca-muted); margin-top: 10px; /* D56 */ }
|
||
.term { margin-top: 16px; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; /* D56 */ }
|
||
.term summary { padding: 11px 14px; cursor: pointer; font-size: 12px; background: var(--ca-surface); /* D56 */ }
|
||
.term summary span { color: var(--ca-muted); /* D56 */ }
|
||
.term pre { margin: 0; padding: 12px 14px; max-height: 180px; overflow: auto; font: 11px/1.7 ui-monospace, monospace;
|
||
color: var(--ca-muted); background: var(--page); white-space: pre-wrap; overflow-wrap: anywhere; /* D56 */ }
|
||
@media (max-width: 700px) {
|
||
.stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); /* D56 */ }
|
||
.outcomes { grid-template-columns: minmax(0, 1fr); /* D56 */ }
|
||
.outcome-files .file-row { grid-template-columns: minmax(0, 1fr); gap: 4px; /* D56 */ }
|
||
}
|
||
|
||
/* Overview: real library statistics and the legacy tool cards inside a concept `.surface`. */
|
||
.stat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin: 0 0 26px; }
|
||
.stat { padding: 22px; background: var(--ca-surface); border: 1px solid var(--line); border-radius: 7px; }
|
||
.stat dt { color: var(--ca-muted); font-size: 12px; }
|
||
.stat dd { margin: 8px 0 0; font-size: 32px; font-weight: 600; }
|
||
.overview-stats > [role="status"], .overview-stats > [role="alert"], .empty-state { padding: 18px 0; color: var(--ca-muted); }
|
||
.overview-stats > [role="alert"] { color: var(--ca-error-text); }
|
||
.overview-stats > button { margin-bottom: 24px; }
|
||
.tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); gap: 12px; margin-top: 22px; }
|
||
.tool-card { border: 1px solid var(--line); background: var(--ca-field); border-radius: 8px; padding: 20px; display: flex; flex-direction: column; gap: 10px; }
|
||
.tool-card p { flex: 1; margin: 0 0 12px; }
|
||
.workspace-link { display: inline-flex; align-items: center; min-height: 44px; padding: 8px 0; font-size: 12px; overflow-wrap: anywhere; }
|
||
.handoff .workspace-link { margin-top: 12px; }
|
||
@media (max-width: 700px) {
|
||
.stat-grid { gap: 8px; }
|
||
.stat { padding: 16px; }
|
||
.stat dd { font-size: 26px; }
|
||
}
|
||
|
||
/* Tags & categories (#245): real vocabulary has unbounded names and bounded lists, which the concept's short samples
|
||
never show. Proven by the 390px layout and long-name cases in workspace-vocabulary.spec.mjs. */
|
||
.vocabulary-main :where(.two-col, .surface, .table-wrap, .operation > div) { min-width: 0; }
|
||
.vocabulary-main .operation h3 { overflow-wrap: anywhere; }
|
||
/* Long tag names wrap inside the Tag cell so the Actions column keeps its intrinsic width; on two-column screens the
|
||
minimum keeps the concept's longest sample ("architecture", about 5em) on one line. */
|
||
.vocabulary-main td:first-child { overflow-wrap: anywhere; min-width: calc(6em + 20px); }
|
||
/* Tags & categories (#245 decision 5): bounded lists continue with Load more; never a scroll anchor, so appended rows push the end away. */
|
||
.vocabulary-main .vocabulary-continuation { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding-block: 15px 0; overflow-anchor: none; }
|
||
.vocabulary-main .vocabulary-continuation p { margin: 0; flex: 1 1 200px; font-size: 12px; color: var(--ca-muted); }
|
||
.vocabulary-main .vocabulary-continuation .vocabulary-error { color: var(--ca-error-text); }
|
||
@media (max-width: 700px) {
|
||
/* One column: the Tag column has no fixed minimum and Actions asks only for its stacked width (the concept's 390px
|
||
rows stack Browse over Rename / merge), so a wider fallback font wraps the name instead of hiding an action. */
|
||
.vocabulary-main td:first-child { min-width: 0; }
|
||
.vocabulary-main td .row-actions { width: min-content; margin-inline-start: auto; }
|
||
}
|
||
|
||
/* Images (#249 C2). Production-only continuation states the concept never shows: a failed page and a load in flight. */
|
||
.infinite-continuation [role="alert"] { color: var(--ca-error-text); }
|
||
.infinite-continuation button[aria-disabled="true"] { opacity: .55; cursor: wait; }
|
||
/* #249 D14: the scope chip names real sources and folders, which can be far longer than "All output folders". */
|
||
.filter-row span.chip { max-width: 100%; min-width: 0; overflow-wrap: anywhere; }
|
||
/* #249 D32: at extreme zoom the 390px shell is narrower than the concept's three layout buttons; they wrap instead of
|
||
scrolling the page sideways (they never wrap while they fit). */
|
||
.segmented { flex-wrap: wrap; max-width: 100%; }
|
||
/* #249 D32: likewise the continuation's text and Load more wrap instead of pushing the button out of the column. */
|
||
.infinite-continuation { flex-wrap: wrap; }
|
||
|
||
/* #249 D18: the concept zooms a hovered card image (1.03 over 0.2s; its reduced-motion rule makes that instant). A touch
|
||
device has no real hover, so a tap must not leave the zoom stuck. */
|
||
@media (hover: none) { .image-open:hover img { transform: none; /* D18 */ } }
|
||
|
||
/* Images cards (#249 C3). #249 D3: bounded rendering mounts only the visible cards and places each one itself, in every
|
||
layout (the concept places only masonry cards), so the grid reserves the collection's height and every card is
|
||
absolute. Columns and gap still come from the concept's `.image-grid` tracks. */
|
||
.image-grid { position: relative; /* D3 */ }
|
||
.image-grid > .media-card { position: absolute; }
|
||
/* #249 D3: a list row's info keeps its own height so it can be measured; centred, it looks as the stretched concept row.
|
||
#249 D31: it may also shrink below a long nowrap title (the h3 ellipsis then applies, as in the grid). */
|
||
.list-grid .media-info { align-self: center; min-width: 0; /* D3, D31 */ }
|
||
/* #249 D31: at extreme zoom a list row is narrower than the concept's 160px art; the art yields so the info stays inside. */
|
||
.list-grid .media-art { width: min(160px, 45%); /* D31 */ }
|
||
/* #249 D31: at extreme zoom the subtitle wraps inside its column instead of running under the Inspect button. */
|
||
.media-info p { overflow-wrap: anywhere; /* D31 */ }
|
||
/* #249 D31: below the narrowest bounded card (120px, reached only at extreme zoom; images.mjs marks the grid) the info
|
||
row stacks its text above Inspect, so neither the title nor the subtitle collapses to nothing beside the button. */
|
||
.narrow-cards .media-info { flex-wrap: wrap; }
|
||
.narrow-cards .media-info > div { flex: 1 0 100%; }
|
||
/* #249 D31: Info and Tag (#249 C2) wrap onto two lines when even the stacked row cannot hold them side by side. */
|
||
.narrow-cards .media-info > .image-card-actions { flex: 0 1 auto; min-width: 0; flex-wrap: wrap; /* D31 */ }
|
||
/* A list row beside its art keeps room for Inspect. */
|
||
.narrow-cards.list-grid .media-art { width: 40%; }
|
||
.narrow-cards.list-grid .media-info { padding-inline: 4px; }
|
||
|
||
/* Legacy video media (the mock does not drop it) takes the concept's image rules in each layout. */
|
||
.image-open video { display: block; width: 100%; height: auto; aspect-ratio: 1.33; object-fit: cover; }
|
||
.masonry-grid .image-open video { aspect-ratio: var(--media-ratio); object-fit: contain; }
|
||
.list-grid .image-open video { height: 95px; aspect-ratio: auto; }
|
||
|
||
/* Images viewer (#249 C6): the concept's `#viewer-dialog` shell (s1008–1058, ≤700 s1456–1474). #249 D34: real media,
|
||
titles and states that the concept's one demo image never shows. */
|
||
/* #249 D34: a real title can wrap (long linked titles and filenames). Above 700px the dialog stacks its header and the
|
||
concept's `calc(100% - 72px)` layout in a column, so a taller header shrinks the layout instead of pushing the toolbar
|
||
out of view; a one-line title gives the concept's boxes. */
|
||
@media (min-width: 701px) {
|
||
.ca-tools #viewer-dialog[open] { display: flex; flex-direction: column; /* D34 */ }
|
||
}
|
||
/* #249 D34: under 200% page zoom the concept's 94dvh outgrows the viewport; the dialog keeps within it. */
|
||
.ca-tools #viewer-dialog { max-height: min(94dvh, 94%); /* D34 */ }
|
||
#viewer-dialog .dialog-header > div { min-width: 0; }
|
||
#viewer-title { overflow-wrap: anywhere; /* D34 */ }
|
||
#viewer-dialog, #viewer-dialog::backdrop { overscroll-behavior: contain; }
|
||
#viewer-details { overscroll-behavior: contain; /* D34 */ }
|
||
/* #249 D34: Viewer.js's stylesheet styles every `.viewer-toolbar` (`position: absolute; bottom: 0; left: 0; right: 0`),
|
||
which lifts the concept's toolbar out of `.viewer-left` to span the whole dialog over the details' last lines. It stays
|
||
in the column under the stage, where the concept's `.viewer-left` flex column and border place it. */
|
||
#viewer-toolbar { position: static; /* D34 */ }
|
||
/* #249 D34: Viewer.js's own toolbar stays hidden (`toolbar: false`); the concept's `.viewer-toolbar` rule would show it. */
|
||
.viewer-container .viewer-hide { display: none; /* D34 */ }
|
||
/* #249 D34: the stage is focusable for arrow-key navigation; a video plays with native controls, an image without
|
||
Viewer.js shows itself (the concept hides it under the viewer), and a load failure covers the stage with Retry. */
|
||
#viewer-stage:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: -2px; }
|
||
#viewer-stage > :is(video, img:not([hidden])) { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: contain; visibility: visible; /* D34 */ }
|
||
.media-failure { position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; background: var(--ca-field); text-align: center; font-size: 12px; }
|
||
/* #249 D34: Download stays the original file's link, drawn as the concept's toolbar button. */
|
||
#viewer-toolbar > .media-download { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 4px 10px; border: 1px solid var(--ca-border); border-radius: 5px; background: var(--ca-control); color: var(--ca-text); font-size: 12px; white-space: nowrap; /* D34 */ }
|
||
#viewer-toolbar > .media-download:hover:not([aria-disabled="true"]) { background: var(--ca-hover); }
|
||
#viewer-toolbar > [aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
|
||
/* #249 D34: after Next → the position status and the unavailability reasons, each on its own centred line. */
|
||
#viewer-toolbar > p { flex-basis: 100%; margin: 0; text-align: center; font-size: 11px; line-height: 1.6; overflow-wrap: anywhere; }
|
||
@media (pointer: coarse) {
|
||
/* #249 D34: coarse pointers keep 40px square viewer targets (the concept's "−" and "+" are narrower). */
|
||
#viewer-dialog :is(.dialog-header > button, #viewer-toolbar > button, #viewer-toolbar > a) { min-width: 40px; min-height: 40px; /* D34 */ }
|
||
/* #249 D34: the details' disclosure summaries and "Download original" keep 40px targets too (the concept's coarse
|
||
rule covers buttons only); the summary keeps its disclosure marker. */
|
||
#viewer-details summary { box-sizing: border-box; min-block-size: 40px; padding-block: 10px; /* D34 */ }
|
||
#viewer-details a { display: inline-flex; align-items: center; min-block-size: 40px; /* D34 */ }
|
||
}
|
||
@media (max-width: 700px) {
|
||
#viewer-toolbar > .media-download { font-size: 10px; /* D34: as the concept's `.viewer-toolbar button` */ }
|
||
}
|
||
|
||
.prompt-text[tabindex]:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 3px; } /* production-only: a confirmed prompt stays keyboard-reachable (#249 D22) */
|
||
|
||
/* Images side panel (#249 D2), production-only. Real prompts can be far longer than the concept's samples, so the sticky
|
||
panel scrolls within the window and its footer stays reachable; where the concept unsticks it, it grows instead. */
|
||
.detail-panel { min-inline-size: 0; max-block-size: calc(100dvh - 40px); overflow: auto; /* D2 */ }
|
||
@media (max-width: 700px) { .detail-panel { max-block-size: none; /* D2 */ } }
|
||
@container images-page (inline-size < 600px) { .detail-panel { position: static; max-block-size: none; /* D2 */ } }
|
||
/* #249 D2/D20: the prompt blocks' mount point is transparent, so their sections sit in `.detail-inner` (panel) and the
|
||
viewer details' wrapper as the concept's do. */
|
||
:is(.detail-inner, #viewer-details > div) > .prompt-inspector { display: contents; /* D20 */ }
|
||
/* #249 D2: real titles and filenames can be one unbroken word; they wrap inside the 310px panel. */
|
||
.panel-heading h2, .detail-inner > p.small { min-inline-size: 0; overflow-wrap: anywhere; }
|
||
|
||
/* #249 D3: production-only rules kept from the retired images.css and inspector.css. Each has a live consumer. */
|
||
/* The inspector wraps real unbroken literals (model paths, raw records) inside the 310px panel and the 340px viewer
|
||
details, and an unavailable download stays visibly inert next to its reason. */
|
||
.file-inspector { min-inline-size: 0; overflow-wrap: anywhere; }
|
||
.file-inspector [aria-disabled="true"] { opacity: .6; cursor: not-allowed; }
|
||
/* #249 D20: the viewer details' pair send and history buttons wrap inside the narrow rail at 200% zoom with wider fonts,
|
||
where the toolkit's `nowrap` would push them past its edge. */
|
||
.ca-tools #viewer-details > button { white-space: normal; /* D20 */ }
|
||
/* The Images page narrows to one column inside a narrow container (the Library-embedded gallery). */
|
||
.images-main { container: images-page / inline-size; }
|
||
@container images-page (inline-size < 600px) {
|
||
.image-workspace { grid-template-columns: minmax(0, 1fr); }
|
||
}
|
||
/* #249 D42: a discovery alert a rescan leaves scrolls clear of the concept's fixed toast (22px inset, 12px at 700px and
|
||
below, two 12px lines at 390px), never under it; the toast itself keeps the concept's placement. */
|
||
.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 96px; /* D42 */ }
|
||
|
||
/* Prompt library cards (#249 PR E): the concept's `article.prompt-card` (s653–708) with real prompts, titles and media. */
|
||
/* #249 D23: real prompts are far longer than the concept's samples, so the card clamps the positive to two lines; copy
|
||
and send keep the full text, and history shows all of it. */
|
||
.prompt-card .prompt-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; /* D23 */ }
|
||
/* #249 D23: the prompt blocks' mount point is transparent, so the section sits in `.card-content` as the concept's does. */
|
||
.card-content > .prompt-inspector { display: contents; /* D23 */ }
|
||
/* #249 D24: the history summary's prompt blocks sit in `article.surface` as the concept's do (app.js a598); the mount
|
||
point keeps its "Saved prompt texts" region name. */
|
||
.library-history article.surface > .prompt-inspector { display: contents; /* D24 */ }
|
||
/* #249 D25: each compared negative's block sits in the concept's `.compare-item` (app.js a600), so its mount point is
|
||
transparent and adds no margin. */
|
||
.compare-item > .prompt-inspector { display: contents; /* D25 */ }
|
||
/* #249 D23: a real title can be long or one unbroken word; it wraps inside the title row (the toolkit's buttons never
|
||
wrap), while the category chip keeps whole words beside it and wraps only a name wider than its share of the row. */
|
||
.card-title button { min-width: 0; white-space: normal; overflow-wrap: anywhere; /* D23 */ }
|
||
.card-title .chip { flex-shrink: 0; max-width: 60%; overflow-wrap: anywhere; /* D23 */ }
|
||
.prompt-card .tags .chip { max-width: 100%; overflow-wrap: anywhere; /* D23 */ }
|
||
/* #249 D23: a parent with one preview, or none, fills the art row instead of leaving the concept's second cell empty. */
|
||
.prompt-card .card-art > :only-child { grid-column: 1 / -1; /* D23 */ }
|
||
.prompt-card .card-art > .small { display: grid; place-content: center; min-width: 0; color: var(--ca-muted); /* D23 */ }
|
||
|
||
/* Prompt library shelf (#249 PR E). #249 D3: rows stay windowed, as the Images grid's do; the concept's `.library-grid`
|
||
tracks and breakpoints lay them out, each row spans them, and appended rows never anchor the scroll. The row's
|
||
`min-inline-size: 0` is load-bearing: it keeps cards out of the concept's ≤700px `1fr` track's min-content. */
|
||
.library-main .library-grid { overflow-anchor: none; /* D3 */ }
|
||
.library-main .library-row { grid-column: 1 / -1; display: grid; grid-template-columns: inherit; gap: 18px; min-inline-size: 0; /* D3 */ }
|
||
.library-main .library-row-spacer { grid-column: 1 / -1; pointer-events: none; /* D3 */ }
|
||
/* #249 D40: the shelf's and history's production-only load, failure and continuation states. */
|
||
.library-main .library-continuation { padding-block: 22px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; /* D40 */ }
|
||
.library-main .library-status { flex: 1 1 240px; color: var(--ca-muted); /* D40 */ }
|
||
|
||
/* History variants panel (#249 PR F). */
|
||
/* D45: the panel's actions wrap their labels at 390px and 200% zoom (the toolkit's buttons never wrap). */
|
||
.ca-tools .library-main .library-history .row-actions button { max-inline-size: 100%; white-space: normal; /* D45 */ }
|
||
/* D45: the panel's paging buttons wrap like the recorded toolbar's. */
|
||
.ca-tools .library-main :where(.timeline-panel, .history-toolbar) > button { max-inline-size: 100%; white-space: normal; /* D45 */ }
|
||
/* D45: archived pairs (#235) are concept lanes at reduced emphasis (the .6 disabled-marker opacity); text stays at full contrast. */
|
||
.library-history .variant-lane.library-variant-archived::before, .ca-tools .library-history .library-variant-archived .variant-dot { opacity: .6; /* D45 */ }
|
||
.ca-tools .library-main .library-variant-restore { position: static; margin-inline-start: 8px; min-block-size: 24px; padding: 2px 8px; font-size: 10px; vertical-align: middle; /* D45 */ }
|
||
@media (pointer: coarse) { .ca-tools .library-main .library-variant-restore { min-block-size: 40px; /* D45 */ } }
|
||
/* D45: at 390px and 200% zoom the panel heading and the legend row wrap, and the All images button wraps its label,
|
||
so the page never scrolls sideways (the retired production footer did the same). */
|
||
.library-history .timeline-panel > .panel-heading, .library-history .timeline-lower { flex-wrap: wrap; /* D45 */ }
|
||
/* D45: while the repeat-run dot shows (left: 190px), the lane text wraps inside the 57px lane before it (43px + 139px). */
|
||
.library-history .variant-lane:has(> .run-dot:not([hidden])) > span { max-inline-size: 139px; overflow-wrap: anywhere; /* D45 */ }
|
||
.ca-tools .library-history .timeline-lower > button { max-inline-size: 100%; white-space: normal; /* D45 */ }
|
||
@media (pointer: coarse) {
|
||
/* D45: a coarse pointer reaches the concept dot through a 47px target. */
|
||
.ca-tools .library-history .variant-dot::after { content: ""; position: absolute; inset: -9px; }
|
||
/* D45: the 12px repeat-run dot (8px inside its border) gets a 44px coarse target the same way. */
|
||
.ca-tools .library-history .run-dot::after { content: ""; position: absolute; inset: -18px; }
|
||
}
|
||
|
||
/* #249 D48: production surfaces the concept never shows keep their own rules, in the concept's tokens: recorded
|
||
executions and the Recorded run dialog (#209, D26), the Archive negative confirmation (#235) and the acknowledged
|
||
prompt sender (spec §2.2). The dialog rules name `dialog` so they win over the concept's `.ca-tools dialog`. */
|
||
/* D48: recorded executions (#209) below the shared gallery. */
|
||
.library-main .library-surface { padding: 22px; background: var(--ca-surface); border: 1px solid var(--ca-line); border-radius: 7px; margin-block: 18px; /* D48 */ }
|
||
.history-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-block: 14px; /* D48 */ }
|
||
.recorded-history > .muted, .history-count, .history-scope, .history-status, .history-legend { font-size: 11px; line-height: 1.65; overflow-wrap: anywhere; /* D48 */ }
|
||
.library-main .recorded-history > h2 { font-size: 14px; line-height: 1.5; /* D48 */ }
|
||
.library-main .recorded-history > h2 + p { font-size: 12px; line-height: 1.5; /* D48 */ }
|
||
.history-timeline { position: relative; box-sizing: border-box; inline-size: 100%; max-inline-size: 100%; max-block-size: 490px; overflow: auto; overscroll-behavior: contain; overflow-anchor: none; border: 1px solid var(--ca-line); border-radius: 6px; background: var(--ca-field); /* D48 */ }
|
||
.history-canvas { position: relative; min-inline-size: calc(100% - 40px); margin: 18px 20px; /* D48 */ }
|
||
.history-positive-lane { position: absolute; inset-block-start: 17px; inset-inline: 0; border-block-start: 2px solid var(--green); color: var(--green); font-size: 10px; line-height: 1.6; padding-inline-start: 38px; /* D48 */ }
|
||
.history-positive-lane::before { content: "+"; position: absolute; inset-block-start: -13px; inset-inline-start: 0; border-radius: 50%; inline-size: 25px; block-size: 25px; background: var(--green); color: #172b22; text-align: center; font-size: 14px; line-height: 21px; /* D48 */ }
|
||
.history-positive-lane > span { position: absolute; inset-block-start: -25px; inset-inline-start: 38px; font-size: 10px; color: var(--green); /* D48 */ }
|
||
.history-earlier-edge { position: absolute; inset: 0 auto 0 0; inline-size: 1px; /* D48 */ }
|
||
.history-context-branch { position: absolute; inset-inline-end: 0; block-size: 57px; border-block-start: 2px solid color-mix(in srgb, var(--red) 60%, transparent); /* D48 */ }
|
||
.history-context-branch::before { content: ""; position: absolute; inset-block-start: -25px; inset-inline-start: -30px; inline-size: 30px; block-size: 25px; border-inline-start: 2px solid var(--red); border-block-end: 2px solid var(--red); border-end-start-radius: 22px; /* D48 */ }
|
||
.history-context-label { position: absolute; inset-inline-start: 43px; inset-block-start: 6px; inline-size: 370px; max-inline-size: calc(100% - 43px); font-size: 10px; line-height: 1.5; color: var(--ca-muted); overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: clip; /* D48 */ }
|
||
.history-run-list { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; /* D48 */ }
|
||
.history-run-column { position: absolute; inset-block: 0; inline-size: 64px; pointer-events: none; /* D48 */ }
|
||
.history-position { position: absolute; inset-block-start: 41px; font-size: 10px; color: var(--ca-muted); /* D48 */ }
|
||
.ca-tools .history-run-marker { position: absolute; pointer-events: auto; transform: translate(-50%, -50%); inline-size: 12px; block-size: 12px; min-inline-size: 12px; min-block-size: 12px; border: 2px solid var(--red); border-radius: 50%; background: var(--ca-field); padding: 0; color: var(--ca-field); font-size: 16px; line-height: 1; /* D48 */ }
|
||
.ca-tools .history-context-anchor { inline-size: 29px; block-size: 29px; min-inline-size: 29px; min-block-size: 29px; background: var(--red); color: #38151b; font-size: 12px; /* D48 */ }
|
||
.history-run-marker[aria-pressed="true"], .history-run-marker:focus-visible { outline: 3px solid var(--ca-focus); outline-offset: 4px; /* D48 */ }
|
||
.history-run-marker[aria-disabled="true"] { opacity: .6; cursor: progress; /* D48 */ }
|
||
/* D48: the Recorded run dialog (#209, D26). */
|
||
.ca-tools dialog.recorded-run-dialog { box-sizing: border-box; inline-size: min(1100px, calc(100% - 32px)); max-inline-size: calc(100% - 32px); max-block-size: calc(100dvh - 32px); padding: 22px; border-radius: 7px; overscroll-behavior: contain; scrollbar-gutter: stable; overflow-wrap: anywhere; /* D48 */ }
|
||
.recorded-run-header { display: flex; gap: 12px; align-items: center; justify-content: space-between; /* D48 */ }
|
||
.recorded-run-header h2 { font-size: 20px; /* D48 */ }
|
||
.recorded-run-body, .recorded-run-body > section { min-inline-size: 0; /* D48 */ }
|
||
.recorded-run-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; font-size: 12px; /* D48 */ }
|
||
.recorded-run-facts dt { color: var(--ca-muted); font-size: 10px; /* D48 */ }
|
||
.recorded-run-facts dd { margin: 4px 0 0; white-space: pre-wrap; /* D48 */ }
|
||
.recorded-run-anchors, .recorded-run-mode { display: flex; flex-wrap: wrap; gap: 8px; margin-block: 18px; /* D48 */ }
|
||
.recorded-run-dialog [aria-pressed="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; /* D48 */ }
|
||
.recorded-run-dialog :where(.muted:not(.prompt-text), .recorded-output-status) { font-size: 11px; line-height: 1.65; /* D48 */ }
|
||
/* D48: the current file's prompt blocks sit below the dialog's evidence line (elsewhere their mount point is `display: contents`). */
|
||
.recorded-run-dialog .prompt-inspector { margin-block-start: 22px; /* D48 */ }
|
||
.recorded-provenance { border-block-start: 1px solid var(--ca-line); padding-block-start: 12px; margin-block: 18px; /* D48 */ }
|
||
.recorded-outputs { margin-block: 24px; /* D48 */ }
|
||
.recorded-receipts { list-style: decimal; padding-inline-start: 24px; /* D48 */ }
|
||
.recorded-receipt { border: 1px solid var(--ca-line); border-radius: 5px; padding: 12px; margin-block: 10px; font-size: 12px; /* D48 */ }
|
||
.recorded-receipt > div { margin-block-end: 10px; /* D48 */ }
|
||
.recorded-receipt[aria-current="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; /* D48 */ }
|
||
.recorded-current-file { border-block-start: 1px solid var(--ca-line); padding-block-start: 16px; /* D48 */ }
|
||
/* D48: the frozen field provenance and the Archive confirmation show stored text in `pre.prompt-text`, not as the
|
||
concept's code `pre`. */
|
||
pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; margin-block: 10px; background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; /* D48 */ }
|
||
/* D48: the Archive negative confirmation's negative block (#235). */
|
||
.prompt-block { border-block-start: 1px solid var(--ca-line); padding-block: 13px; min-inline-size: 0; /* D48 */ }
|
||
.prompt-block-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; /* D48 */ }
|
||
.prompt-block.prompt-negative h3 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; margin: 0; color: var(--red); /* D48 */ }
|
||
.prompt-empty { font-size: 10px; /* D48 */ }
|
||
/* D48: the acknowledged prompt sender (prompt-sender.mjs) and its labelled pair send button. */
|
||
.prompt-send-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: var(--ca-focus); /* D48 */ }
|
||
.prompt-send-button svg { inline-size: 16px; block-size: 16px; flex-shrink: 0; /* D48 */ }
|
||
.ca-tools dialog.prompt-sender { inline-size: min(760px, calc(100% - 32px)); max-inline-size: none; max-block-size: calc(100dvh - 32px); padding: 22px; border-color: var(--ca-line); border-radius: 12px; /* D48 */ }
|
||
.ca-tools dialog.prompt-sender::backdrop { background: #000b; /* D48 */ }
|
||
.prompt-sender-header, .prompt-sender-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; /* D48 */ }
|
||
.prompt-sender-header h2 { font-size: 20px; /* D48 */ }
|
||
.prompt-sender-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-block: 20px; /* D48 */ }
|
||
.prompt-sender-options .ca-field { display: flex; flex-direction: column; align-items: stretch; gap: 6px; /* D48 */ }
|
||
.prompt-sender select { inline-size: 100%; min-block-size: 38px; /* D48 */ }
|
||
.prompt-sender-status { font-size: 13px; line-height: 1.6; /* D48 */ }
|
||
.prompt-sender-status[role="alert"] { color: var(--ca-error-text); /* D48 */ }
|
||
.prompt-sender-preview { margin-block: 20px; min-inline-size: 0; /* D48 */ }
|
||
.prompt-sender-field { border: 1px solid var(--ca-line); border-radius: 7px; padding: 14px; margin-block: 12px; min-inline-size: 0; /* D48 */ }
|
||
.prompt-sender-field :where(pre, textarea), .prompt-sender-composition { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; font-size: 12px; line-height: 1.65; max-inline-size: 100%; /* D48 */ }
|
||
.prompt-sender-field textarea { inline-size: 100%; min-block-size: 100px; resize: vertical; /* D48 */ }
|
||
.prompt-sender-composition { padding: 12px; background: var(--ca-field); border-radius: 6px; /* D48 */ }
|
||
.prompt-sender-actions { justify-content: flex-end; margin-block-start: 18px; /* D48 */ }
|
||
@media (max-width: 780px) {
|
||
.prompt-sender-options { grid-template-columns: minmax(0, 1fr); /* D48 */ }
|
||
}
|
||
@media (max-width: 700px) {
|
||
.ca-tools dialog.recorded-run-dialog { padding: 16px; /* D48 */ }
|
||
.recorded-run-facts { grid-template-columns: minmax(0, 1fr); /* D48 */ }
|
||
}
|
||
@media (pointer: coarse) {
|
||
/* D48: the recorded toolbar, its scope disclosure and the dialog's controls keep 44px targets (concept coarse buttons are 40px). */
|
||
.ca-tools .recorded-history :is(.history-toolbar button, .history-scope summary) { min-inline-size: 44px; min-block-size: 44px; }
|
||
.ca-tools .history-run-marker { inline-size: 44px; block-size: 44px; min-inline-size: 44px; min-block-size: 44px; border: 0; background: transparent; color: transparent; }
|
||
.ca-tools .history-run-marker::before { content: ""; position: absolute; inset: 16px; box-sizing: border-box; border: 2px solid var(--red); border-radius: 50%; background: var(--ca-field); }
|
||
.ca-tools .history-context-anchor::before { content: "−"; inset: 7.5px; inline-size: 29px; block-size: 29px; line-height: 25px; color: #38151b; background: var(--red); }
|
||
.ca-tools .recorded-run-dialog :where(button, summary) { min-inline-size: 44px; min-block-size: 44px; }
|
||
}
|
||
@media (forced-colors: active) {
|
||
.history-context-branch, .history-context-branch::before, .history-run-marker, .history-run-marker::before { border-color: CanvasText; }
|
||
.history-run-marker[aria-pressed="true"] { outline-color: Highlight; }
|
||
}
|
||
|
||
/* Tag image dialog (#249 C2): the concept's `tagging-modal` (s1598–1694) as it stands, but D83 (ruling T4): the
|
||
suggestions are removable chips, each the concept's `.chip` corner (4px; the later `.tag-suggestion` row rule would
|
||
give 5px) around the row's 12px text in its checked suggestion row's tokens (`.tag-suggestion:has(input:checked)`,
|
||
s1655): a name, the confidence and a × button. */
|
||
#tag-suggestions .chip.tag-suggestion { display: inline-flex; gap: 6px; padding: 2px 2px 2px 8px; margin-bottom: 0; cursor: default; color: inherit; border-color: var(--ca-accent); background: var(--soft-blue); border-radius: 4px; /* D83 */ }
|
||
#tag-suggestions .chip.tag-suggestion .tag-confidence { margin-inline-start: 0; /* D83 */ }
|
||
.ca-tools #tag-suggestions .chip.tag-suggestion > button { min-block-size: 24px; min-inline-size: 24px; padding: 0 6px; /* D83 */ }
|
||
@media (pointer: coarse) {
|
||
/* D83: the chip's × keeps the concept's 40px coarse target. */
|
||
.ca-tools #tag-suggestions .chip.tag-suggestion > button { min-block-size: 40px; min-inline-size: 40px; }
|
||
}
|
||
|
||
/* Auto-tag selected images (#249 PR D, D85/D90): the Tag image dialog's model panel, then How to apply, a radio group
|
||
in the concept's `.field` row whose radios keep their own size (the concept's `.field input` is a full-width text
|
||
field) beside their label, as the concept's own radio rows (s1160 `.compare-item label`). */
|
||
.field[role="radiogroup"] > label { display: flex; align-items: center; gap: 8px; /* D90 */ }
|
||
.field[role="radiogroup"] input[type="radio"] { width: auto; margin: 0; /* D90 */ }
|