diff --git a/frontend/src/create/review/Review.svelte b/frontend/src/create/review/Review.svelte
index 081f9fb..74490d1 100644
--- a/frontend/src/create/review/Review.svelte
+++ b/frontend/src/create/review/Review.svelte
@@ -557,6 +557,9 @@
{#if p.id === primaryId}
PRIMARY
{/if}
+ {#if p.required}
+ REQUIRED
+ {/if}
{i + 1}
@@ -824,18 +827,26 @@
.pcard:not(.kept) { opacity: .4; }
.ph-thumb { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.ph-pri { position: absolute; top: 6px; right: 6px; background: #00e5ff; color: #002; font-size: .56rem; font-weight: 800; padding: 1px 5px; border-radius: 2px; letter-spacing: .03em; pointer-events: none; }
+ .ph-req { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); background: #ffb020; color: #201500; font-size: .52rem; font-weight: 800; padding: 1px 5px; border-radius: 2px; letter-spacing: .04em; pointer-events: none; }
.ph-seq { position: absolute; bottom: 6px; left: 6px; width: 18px; height: 18px; border-radius: 50%; background: rgba(0,0,0,.7); color: #fff; font-size: .66rem; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.ph-keep { position: absolute; top: 6px; left: 6px; width: 16px; height: 16px; border: 2px solid #fff; border-radius: 3px; background: rgba(0,0,0,.5); pointer-events: none; }
.pcard.kept .ph-keep { background: #00e5ff; border-color: #00e5ff; }
.pcard.kept .ph-keep::after { content: ''; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid #002; border-width: 0 2px 2px 0; transform: rotate(45deg); }
/* Mandatory photos (clean cal + dimension-overlay): the keep checkbox is
- checked AND disabled — it can't be unchecked, and a small lock glyph
- signals "required, cannot be removed". The card keeps full opacity even
- though it's always kept, and the cursor shows it's not a remove target. */
- .pcard.required { border-color: #2a2a2a; }
+ checked AND disabled. A bold padlock glyph in the box signals "required —
+ cannot be removed", and the card carries a thin required outline so the
+ owner can tell mandatory photos apart from optional batch photos. */
+ .pcard.required { border-color: #00e5ff55; }
.pcard.required .ph-keep { background: #00e5ff; border-color: #00e5ff; }
- .pcard.required .ph-keep::after { content: ''; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid #002; border-width: 0 2px 2px 0; transform: rotate(45deg); }
- .pcard.required .ph-keep::before { content: ''; position: absolute; left: 2.5px; top: 5px; width: 7px; height: 5px; border: 1.5px solid #002; border-radius: 1px; }
+ /* Padlock: shackle (arc) + body, drawn large enough to read on a 16px box. */
+ .pcard.required .ph-keep::after {
+ content: ''; position: absolute; left: 3px; top: 2px; width: 8px; height: 8px;
+ border: 2px solid #002; border-bottom: none; border-radius: 4px 4px 0 0;
+ }
+ .pcard.required .ph-keep::before {
+ content: ''; position: absolute; left: 2.5px; top: 6px; width: 8px; height: 6px;
+ background: #002; border-radius: 1px;
+ }
.pcard.required { cursor: default; }
.ph-nudge { color: #e8a; font-size: .74rem; margin: 8px 0 0; }
diff --git a/frontend/tests/e2e/review-photo-strip.spec.ts b/frontend/tests/e2e/review-photo-strip.spec.ts
index 6f375d1..62d2236 100644
--- a/frontend/tests/e2e/review-photo-strip.spec.ts
+++ b/frontend/tests/e2e/review-photo-strip.spec.ts
@@ -213,12 +213,18 @@ test.describe('M3 Review photo strip (#90)', () => {
expect(bgImage).toContain('45deg');
// Mandatory photos: the clean calibration (seq 1) and the dimension-overlay
- // copy (seq 2) are required — their keep checkbox is checked AND disabled.
+ // copy (seq 2) are required — their keep checkbox is checked AND disabled,
+ // and a REQUIRED tag marks them.
await expect(cards.nth(0)).toHaveAttribute('data-required', 'true');
await expect(cards.nth(1)).toHaveAttribute('data-required', 'true');
await expect(cards.nth(2)).toHaveAttribute('data-required', '');
await expect(cards.nth(0)).toHaveAttribute('aria-pressed', 'true');
await expect(cards.nth(1)).toHaveAttribute('aria-pressed', 'true');
+ await expect(cards.nth(0)).toHaveAttribute('aria-disabled', 'true');
+ await expect(cards.nth(1)).toHaveAttribute('aria-disabled', 'true');
+ await expect(cards.nth(2)).toHaveAttribute('aria-disabled', 'false');
+ // Two REQUIRED tags (cal + overlay), none on the batch photo.
+ await expect(page.getByText('REQUIRED', { exact: true })).toHaveCount(2);
// Tapping a mandatory photo is a no-op: it stays checked + primary (can't remove).
// The card is aria-disabled, so force the click to exercise the handler directly.