/* Viewer: PACS surround, film-black letterbox, amber = learner, cyan = expert truth (SPEC §14.1). Three rows: a prompt strip, the film stage, the image controls. The strips never overlap the film. */ .viewer { position: relative; height: 100%; min-height: 0; display: flex; flex-direction: column; background: var(--pacs-surround); } .topStrip { flex: 0 0 40px; height: 40px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 12px; color: #c9d1d8; font-size: var(--fs-1); white-space: nowrap; overflow: hidden; } :root[data-projector='1'] .topStrip { flex-basis: 48px; height: 48px; } .stripLeft { min-width: 0; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; } .stripRight { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } .stripNote { color: #e6ebef; } .stripArmed { color: #e6ebef; display: inline-flex; align-items: center; gap: 6px; } .stripArmed strong { color: var(--grease-amber); font-weight: 700; } .stripLink { background: none; border: none; color: #c9d1d8; text-decoration: underline; cursor: pointer; padding: 2px 4px; margin-left: 8px; font-size: var(--fs-1); } .key { display: inline-flex; align-items: center; gap: 7px; color: #c9d1d8; } .keyCyan, .keyAmber { display: inline-block; width: 18px; height: 0; border-top: 2px solid var(--lightbox-cyan); } .keyAmber { border-top-color: var(--grease-amber); margin-left: 10px; } .stage { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; touch-action: none; user-select: none; cursor: default; outline: none; } .stage:focus-visible { box-shadow: inset 0 0 0 2px var(--grease-amber); } .marking { cursor: crosshair; } .panning { cursor: grabbing; } .layer { position: absolute; left: 0; top: 0; transform-origin: 0 0; } /* The view eases back to the whole film as the reveal starts (part of the one animated moment). */ .fitting { transition: transform 360ms ease-out; } .film { position: absolute; inset: -1px; background: var(--film-black); box-shadow: 0 0 0 1px #000; } .image, .trace { position: absolute; left: 0; top: 0; display: block; pointer-events: none; } .trace { opacity: 0.45; image-rendering: auto; mix-blend-mode: screen; /* reads as light where you looked, not a stain */ } .overlay { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; } /* Marks: grease-pencil amber. Unfinished marks are dashed. Overcalls get a hollow ring after the reveal. */ .mark { cursor: grab; } .markHalo { fill: none; stroke: rgba(0, 0, 0, 0.55); } .markRing { fill: none; stroke: var(--grease-amber); } .markDot { fill: var(--grease-amber); } .markText { fill: var(--grease-amber); stroke: rgba(0, 0, 0, 0.75); paint-order: stroke; font-weight: 700; } .overcallRing { fill: none; stroke: var(--grease-amber); stroke-opacity: 0.9; } /* Expert outlines: cyan. Labels are plain text on a solid dark pill. */ .outline { fill: var(--lightbox-cyan); fill-opacity: 0.07; stroke: var(--lightbox-cyan); stroke-linejoin: round; } .outlineHalo { fill: none; stroke: var(--lightbox-cyan); stroke-opacity: 0.24; stroke-linejoin: round; } .pill { fill: rgba(9, 11, 13, 0.88); } .findingText { fill: var(--lightbox-cyan); font-weight: 700; } .missChip { fill: none; stroke: var(--lightbox-cyan); } .missChipText { fill: var(--lightbox-cyan); font-weight: 700; } .arrow, .arrowCasing { fill: none; stroke: var(--grease-amber); stroke-linecap: round; stroke-linejoin: round; } .arrowCasing { stroke: rgba(0, 0, 0, 0.55); } .arrowText { fill: var(--grease-amber); font-weight: 700; } .unvisitedRing { fill: none; stroke: var(--grease-amber); stroke-opacity: 0.95; } .ringText { fill: var(--grease-amber); font-weight: 700; } /* ---- Reveal choreography (~1.2 s). The only self-starting motion in the app. ---- */ .revealTrace { animation: fadeTrace 320ms ease-out both; } .revealRing { animation: fade 320ms ease-out 120ms both; } .revealOutline { stroke-dasharray: 1; stroke-dashoffset: 1; fill-opacity: 0; animation: drawFill 480ms ease-in-out 300ms forwards; } .revealHalo { animation: haloPulse 1100ms ease-in-out 780ms both; } .revealLabel { animation: fade 200ms ease-out 760ms both; } .revealArrow { stroke-dasharray: 1; stroke-dashoffset: 1; animation: draw 340ms ease-out 760ms forwards; } .revealArrowHead { stroke-dasharray: 1; stroke-dashoffset: 1; animation: draw 120ms ease-out 1080ms forwards; } .revealArrowText { animation: fade 180ms ease-out 1020ms both; } .settle { animation: fade 200ms ease-out 1100ms both; } @keyframes fadeTrace { from { opacity: 0; } to { opacity: 0.45; } } @keyframes fade { from { opacity: 0; } to { opacity: 1; } } @keyframes draw { to { stroke-dashoffset: 0; } } @keyframes drawFill { 80% { stroke-dashoffset: 0; fill-opacity: 0; } to { stroke-dashoffset: 0; fill-opacity: 0.07; } } /* Two soft beats on a missed outline, then it rests as a faint halo. */ @keyframes haloPulse { 0% { stroke-opacity: 0; } 25% { stroke-opacity: 0.6; } 50% { stroke-opacity: 0.18; } 75% { stroke-opacity: 0.6; } 100% { stroke-opacity: 0.24; } } @media (prefers-reduced-motion: reduce) { .revealTrace, .revealLabel, .revealArrowText, .settle, .revealRing, .revealHalo { animation: none; } .revealOutline, .revealArrow, .revealArrowHead { animation: none; stroke-dashoffset: 0; stroke-dasharray: none; } .revealOutline { fill-opacity: 0.07; } .fitting { transition: none; } } /* Magnifier: 180 px, 2.5×. */ .loupe { position: absolute; left: 0; top: 0; display: none; border-radius: 50%; overflow: hidden; pointer-events: none; background: var(--film-black); box-shadow: 0 0 0 1.5px rgba(140, 153, 166, 0.9), 0 0 0 4px rgba(0, 0, 0, 0.55), 0 6px 18px rgba(0, 0, 0, 0.5); } .loupeImg { position: absolute; left: 0; top: 0; max-width: none; transform-origin: 0 0; } .loupeCross { position: absolute; left: 50%; top: 50%; width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%; border: 1px solid rgba(240, 169, 46, 0.8); } /* The armed finding rides with the cursor: amber, because it is the learner's call. */ .armedTag, .crosshairTag { padding: 2px 9px; border-radius: 999px; background: var(--grease-amber); color: #1d1300; font-size: 14px; font-weight: 700; white-space: nowrap; box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.6); pointer-events: none; } .armedTag { position: absolute; left: 0; top: 0; display: none; z-index: 3; } /* Keyboard crosshair: amber with a dark casing so it shows on lung and on bone. */ .crosshair { position: absolute; left: -22px; top: -22px; width: 44px; height: 44px; pointer-events: none; z-index: 3; } .crosshair::before, .crosshair::after { content: ''; position: absolute; background: var(--grease-amber); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.85); } .crosshair::before { left: 0; top: 21px; width: 44px; height: 2px; } .crosshair::after { left: 21px; top: 0; width: 2px; height: 44px; } .crosshairTag { position: absolute; left: 36px; top: 36px; } .loading { position: absolute; left: 50%; top: 45%; transform: translateX(-50%); color: #c9d1d8; font-size: var(--fs-1); pointer-events: none; } /* Image controls: their own strip under the film. */ .tools { flex: 0 0 auto; min-height: 46px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 6px 12px; background: #15181a; border-top: 1px solid #2a2f34; color: #c9d1d8; font-size: var(--fs-1); } .toolGroup { display: inline-flex; align-items: center; gap: 6px; } .toolLabel { color: #c9d1d8; } .toolSpacer { flex: 1 1 0; } /* The tool group at the right end; when the strip wraps it keeps to the right on its own line. */ .toolRight { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; } .zoom { min-width: 2.9em; text-align: center; font-variant-numeric: tabular-nums; color: #e6ebef; } .slider { display: flex; align-items: center; gap: 6px; } .slider input { width: 64px; accent-color: var(--graticule); } .tool, .toolSq { display: inline-flex; align-items: center; gap: 6px; background: transparent; color: #e6ebef; border: 1px solid #4a535c; border-radius: var(--radius); padding: 3px 9px; cursor: pointer; font-size: var(--fs-1); white-space: nowrap; } .toolSq { width: 28px; height: 28px; padding: 0; justify-content: center; font-size: 18px; line-height: 1; } .toolSq:disabled { opacity: 0.4; cursor: default; } .tool:hover, .toolSq:hover:not(:disabled) { border-color: var(--graticule); } .toolOn { background: #e6ebef; color: #111; border-color: #e6ebef; } /* State is written in the label; the dot repeats it (hollow = off, amber = on). */ .dot { width: 8px; height: 8px; border-radius: 50%; background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; opacity: 0.75; flex-shrink: 0; } .toolOn .dot { opacity: 1; background: var(--grease-amber); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); } .viewer :is(button, input):focus-visible { outline: 2px solid var(--grease-amber); outline-offset: 2px; } .tipAnchor { position: relative; display: inline-flex; } .tip { position: absolute; right: 0; bottom: calc(100% + 10px); width: 290px; padding: 9px 12px; border-radius: var(--radius); background: var(--report-paper); color: var(--report-ink); font-size: 14px; line-height: 1.4; white-space: normal; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); pointer-events: none; z-index: 6; } .tip::after { content: ''; position: absolute; right: 28px; top: 100%; border: 7px solid transparent; border-top-color: var(--report-paper); } .tip .kbdTool { border-color: #9aa5af; color: var(--report-ink); } /* Mark popover: report paper on the dark surround, beside the mark with a thin connector. */ .connector { position: absolute; left: 0; top: 0; pointer-events: none; z-index: 4; overflow: visible; } .connectorCasing { stroke: rgba(0, 0, 0, 0.7); stroke-width: 3.5; stroke-linecap: round; } .connectorLine { stroke: var(--grease-amber); stroke-width: 1.5; stroke-linecap: round; } .connectorDot { fill: var(--grease-amber); stroke: rgba(0, 0, 0, 0.7); stroke-width: 1; } .popover { position: absolute; z-index: 5; background: var(--report-paper); color: var(--report-ink); border-radius: var(--radius); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); padding: 12px 12px 10px; font-size: var(--fs-1); cursor: default; outline: none; } .popover :is(button):focus-visible { outline: 2px solid #9a6200; outline-offset: 1px; } .popHead { display: flex; gap: 8px; align-items: center; font-weight: 700; margin-bottom: 8px; } .popId { color: #9a6200; } .popChange { margin-left: auto; background: none; border: none; color: var(--report-ink-muted); text-decoration: underline; cursor: pointer; padding: 2px 0; font-weight: 400; font-size: 14px; } .popLabels { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 8px; } .popLabelRow { display: flex; align-items: center; gap: 4px; min-width: 0; } .popLabel { flex: 1; min-width: 0; text-align: left; border: 1px solid var(--report-rule); background: #fff; border-radius: 4px; padding: 5px 8px; cursor: pointer; color: var(--report-ink); font-size: var(--fs-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .popLabel:hover { border-color: var(--grease-amber); } .popLabelOn { background: var(--grease-amber); border-color: var(--grease-amber); color: #1d1300; font-weight: 700; } .popNotSure { font-style: normal; color: var(--report-ink-muted); } .popNotSure.popLabelOn { color: #1d1300; } .popAsk { margin: 12px 0 6px; font-weight: 700; } .popConfRow { display: flex; justify-content: center; } .popKeys { margin: 8px 0 0; font-size: 13px; color: var(--report-ink-muted); text-align: center; } .popKeys kbd { font: inherit; padding: 0 4px; border: 1px solid #b9c2ca; border-radius: 3px; background: #fff; } .popFoot { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 10px; } .popMissing { font-size: 13px; color: #7a4d00; text-align: right; flex: 1; } .popDelete { background: none; border: none; color: var(--report-ink-muted); text-decoration: underline; cursor: pointer; padding: 4px 0; } .popDone { background: var(--report-ink); color: #fff; border: none; border-radius: var(--radius); padding: 5px 16px; cursor: pointer; } .popDone:disabled { opacity: 0.4; cursor: default; } /* Read-only reveal (expert review page, case review): everything drawn, nothing animates. */ .still .revealTrace, .still .revealLabel, .still .revealArrowText, .still .settle, .still .revealRing, .still .revealHalo { animation: none; } .still .revealOutline, .still .revealArrow, .still .revealArrowHead { animation: none; stroke-dashoffset: 0; stroke-dasharray: none; } .still .revealOutline { fill-opacity: 0.07; } /* Anatomy (key A, after submit): a near-black line on a 1 px light casing reads on dark lung and bright mediastinum. */ .zone, .zoneCasing { fill: none; stroke-linejoin: round; stroke-linecap: round; } .zoneCasing { stroke: rgba(236, 241, 245, 0.78); pointer-events: none; } .zoneCasingOn { stroke: #fff; } .zone { fill: transparent; stroke: #07090b; pointer-events: all; } .zoneOn { fill: rgba(236, 241, 245, 0.14); } .zoneName { color: #e6ebef; } /* Before the first mark: one quiet line in the strip. */ .nudge { display: inline-flex; gap: 14px; color: #c9d1d8; } .nudge span + span::before { content: '·'; margin-right: 14px; color: #6b7680; } .kbdTool { display: inline-block; margin-left: 6px; padding: 0 5px; border: 1px solid #4a535c; border-radius: 3px; font: inherit; font-size: 12px; line-height: 16px; color: #c9d1d8; } .toolOn .kbdTool { border-color: #9aa5af; color: #333; } /* On-film legend: what the amber trace and the anatomy lines mean. Small, bottom-left, never below 13 px. */ .legend { position: absolute; left: 10px; bottom: 10px; z-index: 2; display: flex; flex-direction: column; gap: 6px; max-width: min(330px, 46%); cursor: default; } .legendChip { display: flex; align-items: flex-start; gap: 8px; padding: 6px 10px; border-radius: var(--radius); background: rgba(9, 11, 13, 0.86); border: 1px solid rgba(140, 153, 166, 0.35); color: #e6ebef; font-size: 13px; line-height: 1.4; user-select: text; } :root[data-projector='1'] .legendChip { font-size: 16px; } .legendSwatch { flex-shrink: 0; width: 14px; height: 14px; margin-top: 2px; border-radius: 50%; background: radial-gradient(circle, #ffe5a6 0%, var(--grease-amber) 45%, rgba(240, 169, 46, 0) 100%); } .legendLink { background: none; border: none; padding: 0; color: #fff; text-decoration: underline; cursor: pointer; font-size: inherit; } .legendLine { display: block; margin-top: 3px; } .legendRing { display: inline-block; width: 12px; height: 12px; margin-right: 6px; border-radius: 50%; border: 1.5px dashed var(--grease-amber); vertical-align: -2px; } .legendKey { display: inline-block; width: 22px; height: 8px; margin-right: 5px; vertical-align: 0; } .legendKey + .legendKey, .legendGap { margin-left: 10px; } /* ---- Volumes (CT / MR): the same room, the volume inside the film area. ---- */ .measuring { cursor: crosshair; } .windowing { cursor: ns-resize; } .markGhost { opacity: 0.45; } /* "Not in the reference": a dotted grey ring, neither cyan (truth) nor amber (learner). */ .unmatchedRing { fill: none; stroke: #b8c2cc; stroke-opacity: 0.9; stroke-linecap: round; } .caliperCasing { stroke: rgba(0, 0, 0, 0.6); stroke-linecap: round; } .caliperLine { stroke: var(--grease-amber); stroke-linecap: round; } .caliperEnd { fill: var(--film-black); stroke: var(--grease-amber); } .caliperText { fill: var(--grease-amber); font-weight: 700; paint-order: stroke; stroke: rgba(0, 0, 0, 0.85); stroke-linejoin: round; font-variant-numeric: tabular-nums; } .stripMeasure { margin-left: 8px; padding: 1px 8px; border-radius: 999px; background: var(--grease-amber); color: #1d1300; font-weight: 700; font-variant-numeric: tabular-nums; } .wlReadout { position: absolute; right: 10px; top: 8px; padding: 2px 8px; border-radius: 4px; background: rgba(9, 11, 13, 0.72); color: #c9d1d8; font-size: var(--fs-1); font-variant-numeric: tabular-nums; pointer-events: none; z-index: 2; } /* Component tints (brain): three cyan opacities under the outline. */ .component { fill: var(--lightbox-cyan); stroke: none; } .compKey { display: inline-flex; gap: 10px; margin-left: 12px; color: #c9d1d8; } .compKey i { display: inline-block; width: 12px; height: 12px; border-radius: 2px; background: var(--lightbox-cyan); vertical-align: -2px; } /* Anatomy on a slice: organ zones tinted in graticule grey at 30 %. */ .zoneTint { fill: var(--graticule); fill-opacity: 0.3; stroke: var(--graticule); stroke-opacity: 0.8; pointer-events: all; } .zoneTintOn { fill-opacity: 0.45; } .legendTint { display: inline-block; width: 14px; height: 10px; border-radius: 2px; background: var(--graticule); opacity: 0.5; margin-right: 6px; vertical-align: -1px; } /* The 2×2 grid. */ .grid { position: absolute; inset: 0; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 2px; background: #000; } .pane { position: relative; overflow: hidden; background: var(--pacs-surround); cursor: default; } .pane:hover { box-shadow: inset 0 0 0 1px var(--graticule); } .paneTag { position: absolute; left: 8px; top: 6px; padding: 1px 7px; border-radius: 4px; background: rgba(9, 11, 13, 0.72); color: #e6ebef; font-size: var(--fs-1); pointer-events: none; } .paneInfo { padding: 14px 16px; color: #c9d1d8; font-size: var(--fs-1); overflow: auto; background: #15181a; } .infoList { margin: 0 0 10px; display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; } .infoList div { display: contents; } .infoList dt { color: #8c99a6; } .infoList dd { margin: 0; color: #e6ebef; font-variant-numeric: tabular-nums; } .infoBadge { display: inline-block; margin: 2px 0 10px; } .infoKey { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 10px; } .infoHint { margin: 0; color: #8c99a6; } /* Slice controls. */ .sliceGroup { display: inline-flex; align-items: center; gap: 10px; } .sliceSlider { width: 180px !important; } .select { background: #1c2024; color: #e6ebef; border: 1px solid #3a4148; border-radius: 4px; padding: 3px 6px; font: inherit; font-size: var(--fs-1); } /* The slice dwell bar: one cell per slice, cyan where the finding lives, amber height = time there. */ .dwell { display: inline-flex; align-items: center; gap: 8px; } .dwellCells { display: inline-flex; align-items: flex-end; gap: 1px; height: 22px; width: 180px; padding: 1px; background: #0c0e10; border-radius: 3px; } .dwellCell { position: relative; flex: 1 1 0; min-width: 2px; height: 100%; padding: 0; border: none; border-radius: 1px; background: #22272c; cursor: pointer; overflow: hidden; } .dwellFinding { background: rgba(94, 234, 255, 0.35); } .dwellMissed { box-shadow: inset 0 0 0 1px var(--grease-amber); background: rgba(94, 234, 255, 0.18); } .dwellCurrent { outline: 1px solid #e6ebef; outline-offset: -1px; } .dwellFill { position: absolute; left: 0; right: 0; bottom: 0; background: var(--grease-amber); opacity: 0.9; } .dwellNote { color: #c9d1d8; max-width: 260px; white-space: normal; line-height: 1.2; } /* ---- Round 5: signs (cyan cues after submit), the Draw tool and outlined marks, the segmented tool strip. ---- */ .sign { pointer-events: visiblePainted; cursor: help; outline: none; } /* With the anatomy on, the zones own the pointer (their names show on hover); the signs stay visible, not hoverable. */ .stage[data-anatomy='1'] .sign { pointer-events: none; } .sign:focus-visible .signText { text-decoration: underline; } .signCasing { fill: none; stroke: rgba(0, 0, 0, 0.6); stroke-linecap: round; stroke-linejoin: round; } .signLine { fill: none; stroke: var(--lightbox-cyan); stroke-linecap: round; stroke-linejoin: round; } .signBand { fill: var(--lightbox-cyan); fill-opacity: 0.25; stroke: var(--lightbox-cyan); stroke-opacity: 0.5; } .signText { fill: var(--lightbox-cyan); font-weight: 700; } .signTip { fill: #e6ebef; font-weight: 400; } .signLeader { stroke: var(--lightbox-cyan); stroke-opacity: 0.8; stroke-linecap: round; } .signTipBox { pointer-events: none; } .signTipBox .pill { stroke: var(--lightbox-cyan); stroke-opacity: 0.6; stroke-width: 1px; } .revealSign { animation: fade 240ms ease-out 700ms both; } /* focusSign(): two beats on the line, then it rests. */ .signPulse .signLine, .signPulse .signBand { animation: signPulse 1500ms ease-in-out both; } @keyframes signPulse { 0% { stroke-width: 2px; stroke-opacity: 1; } 25% { stroke-opacity: 0.25; } 50% { stroke-opacity: 1; } 75% { stroke-opacity: 0.25; } 100% { stroke-opacity: 1; } } .signPulse .pill { stroke: var(--lightbox-cyan); stroke-width: 1.5px; } @media (prefers-reduced-motion: reduce) { .revealSign { animation: none; } .signPulse .signLine, .signPulse .signBand { animation: none; } } .still .revealSign { animation: none; } /* Drawn marks: a closed amber path (filled faintly) with the usual tag at its centroid. Unfinished = dashed. */ .outlineMark { fill: var(--grease-amber); fill-opacity: 0.1; stroke: var(--grease-amber); stroke-linejoin: round; cursor: grab; pointer-events: visiblePainted; } .outlineMarkCasing { fill: none; stroke: rgba(0, 0, 0, 0.55); stroke-linejoin: round; } .outlineMarkSel { fill-opacity: 0.18; } /* The stroke being drawn right now. */ .drawing { cursor: crosshair; } .strokeLive { fill: var(--grease-amber); fill-opacity: 0.08; stroke: var(--grease-amber); stroke-linejoin: round; stroke-linecap: round; } /* Outline verdict chip ("On target", "Partly on it", "Too broad") beside a drawn mark after the reveal. */ .verdictChip { fill: rgba(9, 11, 13, 0.88); stroke: var(--grease-amber); } .verdictText { fill: var(--grease-amber); font-weight: 700; } /* Segmented tools: Point · Draw · Caliper, as one control. */ .segment { display: inline-flex; align-items: stretch; border: 1px solid #3a4046; border-radius: var(--radius); overflow: hidden; } .segment .tool { border: none; border-radius: 0; } .segment .tool + .tool { border-left: 1px solid #3a4046; } .segment .tool:hover { border-color: #3a4046; } .segment .toolOn + .tool { border-left-color: #e6ebef; } /* "My search" off: the legend chip stays, as a plain note. */ .legendMuted { color: #c9d1d8; }