/* Review pages — form controls in the app's own idiom.
 *
 * ⚠️ THESE VALUES ARE COPIED, NOT INVENTED. `.admin-add-form` and the
 * `.users-table` add-row in style.css both use exactly this: var(--bg-input),
 * a 1px var(--border), var(--radius), var(--text), 13px. The review pages
 * shipped with bare <input> and <select> and no class at all, so the browser
 * drew raw white boxes in the middle of a dark app — the one place in Create
 * that looked like a different piece of software.
 *
 * Scoped under .review-page so nothing here can leak into a surface that has
 * already made its own decisions.
 */

.review-page label {
    display: block;
    font-size: 12px;
    color: var(--text-dim);
    margin-bottom: 4px;
}

/* ⚠️ THE SELECT FOLLOWS `.engine-select`, NOT the text inputs. Create already
   has a canonical dropdown — 32px tall, appearance:none, and a drawn chevron
   inset 12px from the right rather than the browser's own, which sits hard
   against the edge. Giving a <select> the text-input's 8px/12px padding made it
   taller than every other dropdown in the app. Values copied from style.css. */
.review-page select {
    height: 32px;
    padding: 0 32px 0 10px;
    background-color: var(--bg-input);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%238a8a8a' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='1,2 6,7 11,2'/></svg>");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
}

.review-page input[type="text"],
.review-page input[type="email"],
.review-page input[type="password"],
.review-page textarea {
    padding: 8px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 13px;
    font-family: inherit;
    box-sizing: border-box;
}

.review-page textarea { resize: vertical; min-height: 60px; }

.review-page input:focus,
.review-page select:focus,
.review-page textarea:focus {
    outline: none;
    border-color: var(--accent);
}

.review-page input::placeholder { color: var(--text-dim); opacity: 0.7; }

/* ⚠️ SPACING. The create form ran its rows together and sat the button hard
   against the last control — the owner's "tight and not in-keeping". The app's
   cards breathe at 1rem+; these rows now do too. */
.review-form {
    display: grid;
    gap: 1rem;
    max-width: 640px;
}

.review-form-row { display: flex; align-items: center; gap: .75rem; }

/* The submit sits on its own line with real air above it, rather than tucked
   under the last field as if it were part of it. */
.review-form-actions { margin-top: .5rem; }

.review-card-title {
    font-size: 12px;
    color: var(--text-dim);
    margin-bottom: .75rem;
}

/* A read-only field whose entire job is to be selected and copied — the
   one-time recipient link. Monospace because it is a token, not prose. */
.review-token-field {
    width: 100%;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12px;
}

/* The REVIEWS nav chip.
 *
 * ⚠️ WHY THIS OVERRIDE IS NEEDED AT ALL. All four chips carry
 * `.nav-drawers-toggle`, which sets the rail-label typography — 10px, 3px
 * tracking, 4px/10px padding. But Tools and Bugs are <button> and Reviews is
 * an <a>, and `.nav-links a` in style.css is MORE SPECIFIC than
 * `.nav-drawers-toggle` (class+element beats class). So every anchor chip
 * silently gets 13px text and 12px side padding instead, and renders taller
 * than the buttons beside it. Restating the values at matching specificity is
 * what puts the lozenge back on the same line as its neighbours.
 *
 * ⚠️ `.nav-admin-link` HAS THE SAME PROBLEM and is deliberately NOT fixed here
 * — it is existing chrome outside this feature, and silently restyling it from
 * a review stylesheet is how a shared surface drifts without anybody deciding.
 * Flagged to the owner instead.
 */
.nav-links a.nav-reviews-btn {
    font-size: 10px;
    letter-spacing: 3px;
    text-transform: uppercase;
    padding: 4px 10px;
    line-height: normal;
    /* ⚠️ MATCHES `.nav-bugs-btn`'s -4px. That negative margin exists so Tools
       and Bugs sit closer to each other than to the rest of the cluster;
       inserting Reviews between them left 16px on the left and 12px on the
       right. Carrying the same -4px makes both gaps 12px, so the trio reads as
       one group rather than a pair with something wedged into it. */
    margin-left: -4px;
}

/* ⚠️ STICKY HEADER ON THESE PAGES ONLY.
 *
 * `.top-nav` is not sticky anywhere in Create — the PROJECT page simply never
 * scrolls the document, because it is a fixed viewport with its own internal
 * scroll areas. Object and review pages are ordinary documents, so the header
 * scrolled away and read as a bug against every other screen in the app.
 *
 * Scoped to a body class rather than made global: making `.top-nav` sticky
 * everywhere would change a surface that has never needed it, on pages other
 * people own.
 */
body.nav-sticky .top-nav {
    position: sticky;
    top: 0;
    /* Above page content, below the context menu (10060) and the frame
       viewer (10065), or the header would sit on top of an open overlay. */
    z-index: 40;
}

/* --- inline comment thread ------------------------------------------------
 *
 * ⚠️ A DIFFERENT PRESENTATION OF THE SAME DATA, not a second system. The badge
 * popover is right for a TILE — a peek without leaving the grid. An object
 * page is a detail page, so the thread belongs down the page with the composer
 * at the end, the way a conversation reads. Same `(target_type, target_id)`,
 * same endpoints; a comment left here appears on the tile and vice versa.
 */
.o-thread { display: flex; flex-direction: column; gap: .9rem; }

.o-comment { display: flex; gap: .7rem; align-items: flex-start; }

.o-comment-avatar {
    flex: 0 0 auto;
    width: 26px; height: 26px;
    border-radius: 50%;
    background: var(--bg-input);
    border: 1px solid var(--border);
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; color: var(--text-dim);
}

.o-comment-body { flex: 1 1 auto; min-width: 0; }

.o-comment-meta {
    font-size: 12px;
    color: var(--text-dim);
    margin-bottom: 2px;
}

.o-comment-text {
    font-size: 13px;
    color: var(--text);
    white-space: pre-wrap;
    word-wrap: break-word;
}

.o-comment-delete {
    background: none; border: 0; cursor: pointer;
    color: var(--text-dim); font-size: 12px; padding: 0 4px;
    opacity: 0;
}
.o-comment:hover .o-comment-delete { opacity: .7; }
.o-comment-delete:hover { opacity: 1; color: var(--danger, #e5484d); }

.o-composer { display: flex; gap: .6rem; margin-top: 1.1rem; }
.o-composer textarea { flex: 1 1 auto; min-height: 44px; }
.o-thread-empty { font-size: 13px; color: var(--text-dim); }

.o-comment-edit-btn {
    background: none; border: 0; cursor: pointer;
    color: var(--text-dim); font-size: 12px; padding: 0 4px;
    opacity: 0;
}
.o-comment:hover .o-comment-edit-btn { opacity: .7; }
.o-comment-edit-btn:hover { opacity: 1; color: var(--text); }
.o-comment-edit textarea { width: 100%; }

/* --- review items ---------------------------------------------------------
 * Three lines, always: kind, name, control. A caption that changes shape row to
 * row makes a grid look broken; a control on its own line stays aligned however
 * long a filename runs.
 */
.o-item-name { font-size: 13px; margin: 2px 0 6px; }
.o-item-action { margin-top: 2px; }

/* ⚠️ HIDDEN IS GREYED, NOT GONE. The whole point of hiding rather than deleting
   is that the assembler can still see what they took out and put it back. An
   item that vanished would be indistinguishable from one that was never added. */
.o-review-item.is-hidden img { opacity: .25; filter: grayscale(1); }
.o-review-item.is-hidden .o-item-name { opacity: .5; }

/* NOT IN THE REVIEW AT ALL — distinct from `is-hidden`, which is a decision
   about something already in it. They look alike on purpose (both mean "not
   what the client sees") and are separate rules on purpose, so changing one
   never silently changes the other.

   ⚠️ The dashed edge is what carries the difference. Greying alone would make
   a waiting item and a hidden item identical on a page that shows both. */
.o-review-item.is-pending { outline: 1px dashed rgba(255,255,255,.18);
                            outline-offset: 2px; }
.o-review-item.is-pending img { opacity: .35; filter: grayscale(1); }
.o-review-item.is-pending .o-item-name { opacity: .6; }

/* ⚠️ AIR UNDER THE SUB-HEADING. Both review pages ran their explanatory line
   straight into the card below it — a paragraph touching a bordered box reads
   as though it belongs inside it. The app's cards sit at 1rem+ from what
   precedes them; these now do too. */
.review-subhead { margin: .35rem 0 1.25rem; }

/* An email address is ~24 characters and the default input was sized for a
   name. Widened rather than made flexible, so the row does not reflow as the
   name field grows. */
.review-email-input { min-width: 260px; }


/* THE WAY OUT — clear of the title, and the project first.
   Sat .5rem above an h1, so the two read as one block and the buttons looked
   like part of the heading. */
.review-backnav { display: flex; align-items: center; gap: .75rem;
                  margin: 0 0 1.6rem; }

/* 🔴 ONE ROW WHOSE HEIGHT DOES NOT DEPEND ON WHETHER THERE IS A GAP.
   min-height mirrors a .btn-sm: 4px padding top and bottom, 1px border either
   side, 12px text at ~1.33 line-height — 26px. Same trick, and same reason, as
   `.card-actions .btn.btn-sm { height: 30px }` in style.css: a row of mixed
   text and controls must not change height as its controls come and go. */
.review-items-head { display: flex; align-items: center; gap: .5rem;
                     min-height: 26px; flex-wrap: wrap; }
.review-topup { display: flex; align-items: center; gap: .5rem; margin: 0; }

/* Quiet, and not mistakable for something to press. The element is a real
   button so that a pending tile is exactly as tall as the tile it becomes. */
.o-review-item.is-pending .btn[disabled] { opacity: .5; cursor: default; }


/* The freshly minted link, under the button that minted it. Accented so it is
   findable at a glance, but a block inside Recipients rather than a card of its
   own — it was a card above the review's contents, which pushed the whole
   review down the page to say something about one recipient. */

/* NAME · LINK · REVOKE, one row per recipient. The link is the row's widest
   part and the only elastic one, so the name and the controls keep their
   positions down the column however long a URL or a name runs. */
/* 🔴 A GRID, NOT A FLEX ROW — because this is a table and it was pretending
   not to be. Under flex, every column sized to its own content, so BOTH the
   name and the "opened …" stamp stole width from the link field: the boxes
   ended at a different x on every row, and the row you had actually opened was
   the shortest one. The owner reported the opened flag; the long name was the
   same bug one column over, and fixing only the reported half would have left
   the fields still ragged.

   Four fixed tracks — name · link · seen · action — so the link box starts and
   ends in the same place whatever any row happens to say. */
.review-recipient { display: grid; align-items: center; gap: .75rem;
                    grid-template-columns: 11rem 1fr 11rem auto;
                    padding: .35rem 0; }
/* ⚠️ ELLIPSIS, WITH THE FULL NAME ON HOVER. A fixed column is what makes the
   rows line up, and a long name has to give way to that — but it must not be
   silently lost, so the template carries `title`. */
.review-recipient-name { min-width: 0; overflow: hidden;
                         text-overflow: ellipsis; white-space: nowrap; }
.review-recipient-link { min-width: 0; font-size: 12px; }
.review-recipient .review-token-field { min-width: 0; width: 100%;
                                        cursor: pointer; }
/* 🔴 A FIXED COLUMN, BECAUSE `auto` MADE THE LINK FIELD JUMP. This sizes to
   its own text, and "opened 2026-09-03 07:52" is nearly twice the width of
   "not opened yet" — so the flexible link box beside it SHRANK on exactly the
   rows where somebody had opened the link, and the fields no longer lined up
   down the card. Reported by the owner 3-Sep.

   The width is the longest string this can hold — "opened " plus a
   `YYYY-MM-DD HH:MM` — so the column never has to resize and the link field
   is the same width on every row whatever has happened to it. */
.review-recipient-seen { font-size: 12px; white-space: nowrap;
                         text-align: right; }
.review-recipient form { margin: 0; }
.review-recipient.is-revoked { opacity: .55; }

/* 🔴 ONE LINK, SEVERAL PEOPLE — the shared link and the people under it.
   ⚠️ `.is-under-shared` is a SEPARATE RULE from `.is-revoked`, not a shade of
   it, for the same reason `is-pending` was kept apart from `is-hidden`: both
   dim a row, and they mean opposite things. A person under a shared link is
   perfectly live; they are indented because they are subordinate, not faded
   because they are dead. Merging them would make the next change to one
   silently change the other. */
.review-recipient.is-under-shared {
  /* 🔴 THE INDENT COMES OUT OF THE NAME COLUMN, NOT OFF THE WHOLE ROW.
     `padding-left` on a grid shrinks its content box, so every column moved
     left — the link, the opened-at and the Revoke button all stopped lining
     up with the group's own row above them, and the name cell lost 1.5rem it
     could not spare: "nobody has opened this yet" wrapped to two lines in it.
     Reported by the owner 3-Sep. Narrowing column ONE by exactly the indent
     keeps columns two to four where they were. */
  grid-template-columns: calc(11rem - 1.5rem) 1fr 11rem auto;
  padding-left: 1.5rem;
  border-left: 2px solid var(--border, #2a2a30);
  margin-left: .35rem;
}

/* The mint form. ⚠️ The name box is the width of the NAME COLUMN below it, so
   the thing you type into sits directly above the list of what you typed. */
.review-add-row { display: flex; align-items: center; gap: .75rem; }
.review-add-row input[type=text] { width: 11rem; }
.review-add-row select { padding: .3rem .4rem; }
/* ⚠️ THE GAP BELOW THE FORM IS A RULE, NOT AN INLINE STYLE. It was
   `style="margin-bottom:1.25rem"` on the old two-form markup and was lost when
   those collapsed into one — the form then sat hard against the first
   recipient row and read as part of the list (owner, 3-Sep). Kept here so the
   next rewrite of the markup cannot drop it again. */
.review-add-row { margin-bottom: 1.5rem; }

/* 🔴 HIDDEN, NOT REMOVED-BY-JS. base.html renders flashes inline above the
   content, so "Link minted for X." pushed the whole review down and moved the
   page out from under the cursor. Hiding in CSS means there is no flash of
   content before object_links.js re-raises it as a real toast. */
body.reviews-toasts .flash-messages { display: none; }

/* The outline flash stays as a quiet second signal beside the toast — on its
   own it was not feedback at all: the owner could not tell whether clicking
   had copied. */
.review-token-field.is-copied { border-color: var(--accent); }

/* 🔴 THE SAME SHAPE AS EVERY OTHER TILE IN THE APP: a 16:9 box, black behind,
   the picture CONTAINED inside it. `style.css`'s `.image-container` /
   `.video-container` are exactly this, and a review grid that framed its
   pictures differently from the project page read as a different product.

   ⚠️ IT WAS `aspect-ratio: 4/3` AND `object-fit: cover`, and both were wrong.
   Cover CROPS — so a 2x2 look composite lost a quadrant and a portrait image
   lost its top and bottom, in the one place somebody is deciding what a client
   sees. Letterboxing is not a compromise here; it is the only framing that
   shows the whole picture.

   ⚠️ THIS ALSO RESERVES THE BOX BEFORE THE BYTES ARRIVE, which is what the
   rule it replaces was for. It does it better: `aspect-ratio` on a
   `width: 100%` element gives a definite height for EVERY tile, not only the
   ones whose dimensions the database happens to know, so the grid no longer
   grows under the reader as images land. The width/height attributes stay on
   the tag — they are what a browser with CSS disabled uses, and they cost
   nothing.

   ⚠️ Specificity ties with `.objlink-frame img` in object_links.css, so this
   wins ONLY because base.html loads reviews.css second. Do not reorder those
   two links without checking here. */
.o-review-item img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: contain;
    background: #000;
    display: block;
    border-radius: 6px;
}

/* WHAT THE CLIENT SAID — a count on the tile, the conversation in a panel.
 *
 * 🔴 THE TILE MUST NOT GROW WITH THE THREAD. The notes rendered inline here
 * first and did to this grid what they did to the client's: three notes on one
 * tile and the row grows, four tiles at four different heights, the whole card
 * taller every time somebody says something. Owner, 3-Sep: "going to get out of
 * hand". The control is a `.btn.btn-sm` — the same element as Hide from review,
 * so the two line up by construction rather than by a number kept in step.
 *
 * ⚠️ THE INLINE VERSION'S STYLES WERE DELETED BY ACCIDENT BEFORE THEY WERE
 * REPLACED, and that is worth recording because it explains a screenshot. An
 * edit that rewrote this file from a matched rule "to the end" ate the notes
 * block sitting after it, so reviewer, time and text rendered at one size in
 * one colour — some of what the owner reported as hard to read was CSS that
 * was no longer there. A `[index:]` replacement is a delete of everything
 * below it; use an anchored range.
 */
.o-note-n { font-weight: 600; margin-right: 2px; }

/* The reader. Deliberately the same shape as `/r/`'s panel — an assembler
 * checking what a client said should meet what the client met — and
 * deliberately NOT the same code: the public page loads none of this app's
 * JavaScript, and that separation is the point of it. */
.rn { display: none; position: fixed; inset: 0; z-index: 10070;
      background: rgba(0, 0, 0, 0.72); padding: 40px 20px; }
.rn.open { display: flex; align-items: center; justify-content: center; }
.rn-inner {
    position: relative;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    width: 100%; max-width: 540px; max-height: 100%;
    display: flex; flex-direction: column;
    padding: 18px 20px 20px;
}
.rn-x {
    position: absolute; top: 10px; right: 14px;
    background: transparent; border: 0; color: var(--text-dim);
    font-size: 24px; line-height: 1; cursor: pointer;
}
.rn-x:hover { color: var(--text); }
.rn-title { font-size: 15px; font-weight: 600; margin-top: 2px;
            padding-right: 28px; }
/* Only the thread scrolls, so a long conversation cannot push the panel off
   the screen. */
.rn-thread { overflow-y: auto; margin-top: 14px; min-height: 0; }

/* 🔴 THREE LEVELS, NOT ONE. Reviewer, time and note all rendered at one size in
   one colour, and the owner's word for it was "tough to read" — with three
   notes on a tile it was a wall. The name is the loudest thing, the time is the
   quietest, and the note itself is body text at the size the rest of the page
   is read at. This is not decoration; it is the difference between scanning a
   thread and parsing one. */
.rn-note { border-left: 2px solid var(--accent);
           padding: 0 0 0 .7rem; margin-bottom: 1rem; }
.rn-note-head { display: flex; align-items: baseline; gap: .5rem; }
.rn-who { font-weight: 600; font-size: 13px; }
.rn-when { font-size: 11px; color: var(--text-dim); }
.rn-text { margin: .2rem 0 0; font-size: 14px; white-space: pre-wrap; }

/* ⚠️ TWO SETTING ROWS IN ONE CARD NEED AIR BETWEEN THEM. `Links expire` and
   `Clients may` ran straight into each other and read as one wrapped control
   rather than two decisions. Margin on the sibling, so a card with one row is
   unchanged. */
.review-form-row + .review-form-row { margin-top: .75rem; }

/* Reorder affordance on the review grid. ⚠️ `grab`, not `move` — the tile is
   also a link to the object, so the cursor has to say "you may drag this"
   without claiming that dragging is all it does. */
.o-review-items .o-review-item { cursor: grab; }
.o-review-items .o-review-item.is-dragging { opacity: .4; cursor: grabbing; }
