feat: 반려 키보드 완결 — 가장 중요한 경로가 가장 느렸다
승인·보류는 A/H 한 키에 자동 다음 케이스 이동까지 되는데 반려만 메모가
필수라 흐름이 끊겼다. R 은 메모 필드에 포커스만 줬고, 입력 중에는 전역
단축키가 막히므로(J/K 가 글자로 들어가면 안 되니 의도된 동작) 거기서부터는
마우스로만 제출할 수 있었다. 저작권 심사에서 법적으로 가장 중요한 판정이
가장 느린 경로였다.
R 사유 선택 모드 (프리셋에 1~9 번호가 보인다)
1~9 그 사유로 즉시 반려 + 다음 미심사 케이스로
M 직접 입력으로 전환
Esc 취소
메모 안에서 Ctrl+Enter 자유 서술 반려 제출
화면의 번호가 곧 눌러야 할 키다. 프리셋 칩에 키 배지를 붙여 단축키를 외우지
않아도 되게 했다.
숫자는 사유 선택 모드 안에서만 의미를 갖는다. 모드 밖에서 숫자에 의미를 주면
잘못 누른 키 하나로 반려가 나간다 — 되돌릴 수 있는 동작이지만 주의목록 항목을
만들고 감사에 남으므로 사고로 일어나서는 안 된다. 가드가 숫자 분기보다 먼저
오는지를 테스트가 위치로 확인한다.
프리셋이 하나도 없으면 고를 것이 없으므로 모드를 열지 않고 예전처럼 메모
필드로 보낸다.
실렌더(Playwright 1440x900, 실제 서버 + 부트스트랩 스텁):
R -> 2 {"decision":"rejected","memo":"라이선스 확인 불가로 반려"}
메모 + Ctrl+Enter {"decision":"rejected","memo":"자유 서술 사유"}
콘솔 에러 0.
검증: ruff clean, pytest 645 passed (정적 3건 추가).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
This commit is contained in:
parent
0c60369da0
commit
e169e24584
7 changed files with 216 additions and 8 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
# Project INDEX
|
# Project INDEX
|
||||||
|
|
||||||
|
- 2026-09-21 · **반려 키보드 완결** — 승인·보류는 1타인데 반려만 메모 필수라 손이 마우스로 갔다(8/8 평가 P2-10). `R` 이 메모 필드에 포커스만 주고, 입력 중에는 전역 단축키가 막혀서 거기서부터는 **마우스로만 제출**할 수 있는 구조였다 — 법적으로 가장 중요한 경로가 가장 느렸다. `R` → 사유 선택 모드(프리셋 1~9 번호 표시) → 숫자 1타로 반려 완료, 자유 서술은 메모 안에서 `Ctrl+Enter`. 화면의 번호가 곧 단축키가 되도록 프리셋 칩에 키 배지를 붙였다(외울 필요 없음). **숫자는 사유 선택 모드 안에서만 의미를 갖는다** — 모드 밖에서 잘못 누른 키 하나로 반려가 나가면 안 되고, 그 가드 순서를 테스트로 고정했다. 실렌더(Playwright): `R`→`2` 로 `{"decision":"rejected","memo":"라이선스 확인 불가로 반려"}`, 메모+`Ctrl+Enter` 로 `{"decision":"rejected","memo":"자유 서술 사유"}`, 콘솔 에러 0. 반려 후 승인·보류와 같이 다음 미심사 케이스로 이동한다. **ruff clean · pytest 645 passed**(정적 3건)
|
||||||
|
|
||||||
- 2026-09-21 · **점수 구성 패널** — "증거 51개인데 왜 35점"에 답한다. 스코어러가 풀 상한(v2/v3)과 신뢰 출처 제외(v4)를 갖게 되면서 증거 개수와 점수의 간극이 벌어졌는데, 그 이유가 화면 어디에도 없었다 — 제가 그 간극을 만든 장본인이라 자체 부채였다. `ScoreResult.breakdown` 을 신설해 종류별 기여·풀 상한에 삼켜진 점수·제외된 행을 기록하고, 제출물에 `riskBreakdown` 으로 영속화한 뒤 증거 요약과 증거 목록 **사이**(그 질문이 생기는 지점)에 렌더한다. 실렌더 예: `지식DB 지문 매칭 80 / 웹 게시(google) 60 (원래 125 · 상한 60) / 웹 엔티티 라벨 20 (원래 114 · 상한 20) / 신뢰 출처 posar.info 3행 제외 / 합계 115점이 상한 100에 걸렸습니다`. **점수는 하나도 안 바뀐다** — 기존 스코어러 테스트 31건이 그대로 통과하므로 `SCORER_VERSION` 은 v4 유지(가중치·버킷·밴드 불변). 분해 합계가 항상 점수와 일치하는지도 테스트로 고정했다(분해가 다른 이야기를 하면 분해가 아니라 또 하나의 주장이 된다). **ruff clean · pytest 642 passed**(스코어러 5 + 정적 1)
|
- 2026-09-21 · **점수 구성 패널** — "증거 51개인데 왜 35점"에 답한다. 스코어러가 풀 상한(v2/v3)과 신뢰 출처 제외(v4)를 갖게 되면서 증거 개수와 점수의 간극이 벌어졌는데, 그 이유가 화면 어디에도 없었다 — 제가 그 간극을 만든 장본인이라 자체 부채였다. `ScoreResult.breakdown` 을 신설해 종류별 기여·풀 상한에 삼켜진 점수·제외된 행을 기록하고, 제출물에 `riskBreakdown` 으로 영속화한 뒤 증거 요약과 증거 목록 **사이**(그 질문이 생기는 지점)에 렌더한다. 실렌더 예: `지식DB 지문 매칭 80 / 웹 게시(google) 60 (원래 125 · 상한 60) / 웹 엔티티 라벨 20 (원래 114 · 상한 20) / 신뢰 출처 posar.info 3행 제외 / 합계 115점이 상한 100에 걸렸습니다`. **점수는 하나도 안 바뀐다** — 기존 스코어러 테스트 31건이 그대로 통과하므로 `SCORER_VERSION` 은 v4 유지(가중치·버킷·밴드 불변). 분해 합계가 항상 점수와 일치하는지도 테스트로 고정했다(분해가 다른 이야기를 하면 분해가 아니라 또 하나의 주장이 된다). **ruff clean · pytest 642 passed**(스코어러 5 + 정적 1)
|
||||||
|
|
||||||
- 2026-09-21 · **증거 일괄 표시** (`POST /api/evidence/status-batch`) — 운영자 되먹임이 순증 노동이던 문제를 끝낸다. 8/8 평가 기준으로 실 DB 제출물 하나를 100→35 로 내리려면 google 증거 **51행을 개별 클릭**해야 했다(단건 API 만 존재). 증거 그룹 헤더의 개수 옆에 `전부 무관` 1타를 붙였다. 서버는 **한 트랜잭션 · 리스코어 1회 · 감사 이벤트 1건** — 51행이면 감사도 51건이 되어 500행 캡을 한 번의 클릭으로 먹는다. 행별 who/when/note 는 그대로 남으므로 개별 행이 설명 불가가 되지는 않는다. 남의 증거 id 가 섞이면 **아무것도 표시하지 않고 전체 실패**(절반만 반영되면 운영자가 무엇이 됐는지 알 수 없다), 상한 500(일괄 판정 200 과 단위가 다름 — 그쪽은 케이스 수, 이쪽은 한 케이스의 증거 행 수). 그룹 액션은 `irrelevant`(무관)로 보낸다 — 행별 "미사용"은 `false_positive`(오탐)에 매핑되는데, 제공자 결과 전체를 오탐이라 단정하는 건 과한 주장이다. 실렌더(Playwright 1440×900): 51행 그룹에서 1타로 `{"status":"irrelevant","evidence_ids":[51개]}` 전송, 콘솔 에러 0. **ruff clean · pytest 636 passed**
|
- 2026-09-21 · **증거 일괄 표시** (`POST /api/evidence/status-batch`) — 운영자 되먹임이 순증 노동이던 문제를 끝낸다. 8/8 평가 기준으로 실 DB 제출물 하나를 100→35 로 내리려면 google 증거 **51행을 개별 클릭**해야 했다(단건 API 만 존재). 증거 그룹 헤더의 개수 옆에 `전부 무관` 1타를 붙였다. 서버는 **한 트랜잭션 · 리스코어 1회 · 감사 이벤트 1건** — 51행이면 감사도 51건이 되어 500행 캡을 한 번의 클릭으로 먹는다. 행별 who/when/note 는 그대로 남으므로 개별 행이 설명 불가가 되지는 않는다. 남의 증거 id 가 섞이면 **아무것도 표시하지 않고 전체 실패**(절반만 반영되면 운영자가 무엇이 됐는지 알 수 없다), 상한 500(일괄 판정 200 과 단위가 다름 — 그쪽은 케이스 수, 이쪽은 한 케이스의 증거 행 수). 그룹 액션은 `irrelevant`(무관)로 보낸다 — 행별 "미사용"은 `false_positive`(오탐)에 매핑되는데, 제공자 결과 전체를 오탐이라 단정하는 건 과한 주장이다. 실렌더(Playwright 1440×900): 51행 그룹에서 1타로 `{"status":"irrelevant","evidence_ids":[51개]}` 전송, 콘솔 에러 0. **ruff clean · pytest 636 passed**
|
||||||
|
|
|
||||||
|
|
@ -1037,3 +1037,39 @@ def test_operator_gui_explains_how_the_score_was_reached():
|
||||||
assert "원래" in script and "상한" in script
|
assert "원래" in script and "상한" in script
|
||||||
# 100 포화는 그 자체로 알려야 한다 - 그 구간은 서로 구별되지 않는다.
|
# 100 포화는 그 자체로 알려야 한다 - 그 구간은 서로 구별되지 않는다.
|
||||||
assert "score-breakdown-clamped" in script
|
assert "score-breakdown-clamped" in script
|
||||||
|
def test_operator_gui_completes_the_rejection_path_from_the_keyboard():
|
||||||
|
# 승인·보류는 1타인데 반려만 메모 필수라 손이 마우스로 갔다 — 법적으로 가장
|
||||||
|
# 중요한 경로가 가장 느렸다. R 로 사유를 고르면 두 키로 끝난다.
|
||||||
|
script = _read_app()
|
||||||
|
markup = _read(INDEX)
|
||||||
|
styles = _read(STYLES)
|
||||||
|
|
||||||
|
assert "openRejectPrompt" in script
|
||||||
|
assert "rejectWithPreset" in script
|
||||||
|
assert "closeRejectPrompt" in script
|
||||||
|
assert 'id="reject-prompt"' in markup
|
||||||
|
assert ".reject-prompt" in styles
|
||||||
|
|
||||||
|
# 자유 서술은 메모 안에서 끝나야 한다 — 입력 중에는 전역 단축키가 막힌다.
|
||||||
|
assert 'event.key !== "Enter"' in script
|
||||||
|
assert "R : 반려 사유 선택" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_gui_only_treats_digits_as_reject_reasons_inside_the_prompt():
|
||||||
|
# 모드 밖에서 숫자에 의미를 주면 잘못 누른 키 하나로 반려가 나간다.
|
||||||
|
script = _read_app()
|
||||||
|
|
||||||
|
assert "isRejectPromptOpen" in script
|
||||||
|
prompt_guard = script.index("if (isRejectPromptOpen) {")
|
||||||
|
digit_branch = script.index('if (key >= "1" && key <= "9") {')
|
||||||
|
assert prompt_guard < digit_branch, "숫자 처리는 반드시 모드 가드 안에 있어야 한다"
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_gui_numbers_the_memo_presets_to_match_the_shortcuts():
|
||||||
|
# 화면의 번호와 눌러야 할 키가 다르면 단축키는 외워야 하는 것이 된다.
|
||||||
|
script = _read_app()
|
||||||
|
styles = _read(STYLES)
|
||||||
|
|
||||||
|
assert "memo-preset-key" in script
|
||||||
|
assert ".memo-preset-key" in styles
|
||||||
|
assert "index + 1" in script
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,19 @@ function bindEvents() {
|
||||||
document.getElementById("upload-submission-image").addEventListener("click", uploadSubmissionImage);
|
document.getElementById("upload-submission-image").addEventListener("click", uploadSubmissionImage);
|
||||||
document.getElementById("rerun-enrichment").addEventListener("click", rerunEnrichment);
|
document.getElementById("rerun-enrichment").addEventListener("click", rerunEnrichment);
|
||||||
|
|
||||||
|
// 입력 중 전역 단축키가 막히는 것은 의도된 동작이다(J/K 가 글자로 들어가면
|
||||||
|
// 안 된다). 다만 그 때문에 자유 서술 반려는 메모를 친 뒤 마우스로 가야 했다.
|
||||||
|
// Ctrl+Enter 하나만 열어 그 경로를 키보드로 끝낸다.
|
||||||
|
document.getElementById("decision-memo").addEventListener("keydown", (event) => {
|
||||||
|
|
||||||
|
if (!(event.ctrlKey || event.metaKey) || event.key !== "Enter") return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setDecision("rejected", true).then(() => moveToNextUnreviewed());
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
// 선례 승계 버튼은 케이스마다 다시 그려지므로 컨테이너에 위임한다.
|
// 선례 승계 버튼은 케이스마다 다시 그려지므로 컨테이너에 위임한다.
|
||||||
document.getElementById("prior-decision-box").addEventListener("click", (event) => {
|
document.getElementById("prior-decision-box").addEventListener("click", (event) => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ function renderMemoPresets() {
|
||||||
|
|
||||||
.map(
|
.map(
|
||||||
|
|
||||||
(preset) =>
|
// 화면의 번호가 곧 단축키다. R 로 사유 선택에 들어간 뒤 같은 숫자를
|
||||||
|
// 누르면 그 사유로 반려된다. 10번째부터는 번호 없이 마우스로만 쓴다.
|
||||||
|
(preset, index) =>
|
||||||
|
|
||||||
`<button type="button" class="memo-preset-chip" data-preset-id="${escapeHtml(preset.id)}" title="${escapeHtml(preset.body)}">${escapeHtml(preset.label)}</button>`,
|
`<button type="button" class="memo-preset-chip" data-preset-id="${escapeHtml(preset.id)}" title="${escapeHtml(preset.body)}">${index < 9 ? `<span class="memo-preset-key">${index + 1}</span>` : ""}${escapeHtml(preset.label)}</button>`,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ const KEYBOARD_HELP = [
|
||||||
|
|
||||||
"K / ↑ : 이전 케이스",
|
"K / ↑ : 이전 케이스",
|
||||||
|
|
||||||
"A : 승인 H : 보류 R : 반려(메모로 이동)",
|
"A : 승인 H : 보류 R : 반려 사유 선택",
|
||||||
|
" 사유 선택 중 — 1~9 : 사유 선택 후 즉시 반려 · M : 직접 입력 · Esc : 취소",
|
||||||
|
" 메모 입력 중 — Ctrl+Enter : 반려 제출",
|
||||||
|
|
||||||
"N : 다음 미심사 케이스",
|
"N : 다음 미심사 케이스",
|
||||||
|
|
||||||
|
|
@ -199,16 +201,61 @@ function handleReviewShortcut(event) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRejectPromptOpen) {
|
||||||
|
|
||||||
|
// 사유 선택 중에는 숫자가 곧 사유다. 이 모드 밖에서는 숫자에 아무 의미도
|
||||||
|
// 주지 않는다 - 잘못 누른 키 하나로 반려가 나가면 안 된다.
|
||||||
|
if (key >= "1" && key <= "9") {
|
||||||
|
|
||||||
|
rejectWithPreset(Number(key) - 1);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "m" || key === "M") {
|
||||||
|
|
||||||
|
closeRejectPrompt();
|
||||||
|
|
||||||
|
const memoField = document.getElementById("decision-memo");
|
||||||
|
|
||||||
|
if (memoField) memoField.focus();
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "Escape") {
|
||||||
|
|
||||||
|
closeRejectPrompt();
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (key === "r" || key === "R") {
|
if (key === "r" || key === "R") {
|
||||||
|
|
||||||
// Rejection needs a memo, so hand the operator the field instead of firing
|
// 반려는 메모가 필수라 한 키로 끝낼 수 없다. 그렇다고 메모 필드에 포커스만
|
||||||
// a request that would fail validation.
|
// 주면 거기서부터는 마우스로만 제출할 수 있다(입력 중에는 단축키가 막힌다) -
|
||||||
|
// 법적으로 가장 중요한 경로가 가장 느린 이유였다. 사유 선택을 한 단계 두어
|
||||||
|
// 자주 쓰는 사유는 두 키로, 자유 서술은 Ctrl+Enter 로 끝나게 한다.
|
||||||
switchView("workbench");
|
switchView("workbench");
|
||||||
|
|
||||||
const memo = document.getElementById("decision-memo");
|
if (!openRejectPrompt()) {
|
||||||
|
|
||||||
if (memo) memo.focus();
|
const memo = document.getElementById("decision-memo");
|
||||||
|
|
||||||
|
if (memo) memo.focus();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
|
@ -462,3 +509,69 @@ async function applyBulkDecision(decision) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- 반려 사유 선택 -----------------------------------------------------------
|
||||||
|
|
||||||
|
let isRejectPromptOpen = false;
|
||||||
|
|
||||||
|
function openRejectPrompt() {
|
||||||
|
|
||||||
|
const box = document.getElementById("reject-prompt");
|
||||||
|
|
||||||
|
const presets = decisionMemoPresets.slice(0, 9);
|
||||||
|
|
||||||
|
// 고를 것이 없으면 모드를 열지 않는다. 그때는 예전처럼 메모 필드로 보낸다.
|
||||||
|
if (!box || !presets.length) return false;
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
|
||||||
|
<strong>반려 사유 선택</strong>
|
||||||
|
|
||||||
|
<ol>${presets.map((preset, index) => `<li><span class="memo-preset-key">${index + 1}</span>${escapeHtml(preset.label)}</li>`).join("")}</ol>
|
||||||
|
|
||||||
|
<span class="muted">M : 직접 입력 · Esc : 취소</span>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
box.hidden = false;
|
||||||
|
|
||||||
|
isRejectPromptOpen = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRejectPrompt() {
|
||||||
|
|
||||||
|
const box = document.getElementById("reject-prompt");
|
||||||
|
|
||||||
|
if (box) {
|
||||||
|
|
||||||
|
box.innerHTML = "";
|
||||||
|
|
||||||
|
box.hidden = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
isRejectPromptOpen = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectWithPreset(index) {
|
||||||
|
|
||||||
|
const preset = decisionMemoPresets[index];
|
||||||
|
|
||||||
|
if (!preset) return;
|
||||||
|
|
||||||
|
const memo = document.getElementById("decision-memo");
|
||||||
|
|
||||||
|
if (!memo) return;
|
||||||
|
|
||||||
|
closeRejectPrompt();
|
||||||
|
|
||||||
|
memo.value = preset.body;
|
||||||
|
|
||||||
|
// 승인·보류와 같은 흐름으로 끝낸다 - 판정하고 다음 미심사 케이스로.
|
||||||
|
setDecision("rejected", true).then(() => moveToNextUnreviewed());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,7 @@
|
||||||
<textarea id="decision-memo" rows="3" placeholder="반려 또는 보정 시 메모 필수"></textarea>
|
<textarea id="decision-memo" rows="3" placeholder="반려 또는 보정 시 메모 필수"></textarea>
|
||||||
</label>
|
</label>
|
||||||
<div class="memo-presets" id="memo-presets" aria-label="자주 쓰는 사유"></div>
|
<div class="memo-presets" id="memo-presets" aria-label="자주 쓰는 사유"></div>
|
||||||
|
<div class="reject-prompt" id="reject-prompt" hidden aria-live="polite"></div>
|
||||||
<p class="form-error" id="memo-error" aria-live="assertive"></p>
|
<p class="form-error" id="memo-error" aria-live="assertive"></p>
|
||||||
<div class="decision-actions">
|
<div class="decision-actions">
|
||||||
<button class="approve-action" type="button" data-decision="approved">승인</button>
|
<button class="approve-action" type="button" data-decision="approved">승인</button>
|
||||||
|
|
|
||||||
|
|
@ -3066,6 +3066,47 @@ tbody tr.selected-row,
|
||||||
읽히게 한다 — 행마다 누르던 노동이 여기서 없어진다. */
|
읽히게 한다 — 행마다 누르던 노동이 여기서 없어진다. */
|
||||||
/* 점수 구성. 증거 목록과 점수 사이의 간극을 메우는 자리라, 증거 요약 바로
|
/* 점수 구성. 증거 목록과 점수 사이의 간극을 메우는 자리라, 증거 요약 바로
|
||||||
아래에 둔다. */
|
아래에 둔다. */
|
||||||
|
/* 반려 사유 선택. 승인·보류가 1타인데 반려만 마우스로 끝나던 비대칭을 없앤다 —
|
||||||
|
법적으로 가장 중요한 경로가 가장 느릴 이유가 없다. */
|
||||||
|
.reject-prompt {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--red-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--red-tint);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-prompt ol {
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-prompt li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 화면의 번호가 곧 단축키다. 눌러야 할 키를 키처럼 보이게 한다. */
|
||||||
|
.memo-preset-key {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
padding: 0 3px;
|
||||||
|
border: 1px solid var(--rail-ink);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--rail-ink-strong);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
.score-breakdown {
|
.score-breakdown {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue