재배치 코퍼스는 커밋된 sheetJson 폴더다. 케이스 한 벌이 무엇으로 이루어지고,
두 입구(addTestCase·importCase)가 어디서 갈라져 어디서 합류하는지,
왜 글자 축소를 끄고 렌더하는지, 그리고 굽기 전의 라벨이 왜 거짓말인지를
코드에서 정리했다.
eval-out/cases/<id>/ 폴더 하나. 폴더 이름은 언제나 design_object_id 라 한 컴포넌트에 케이스가 하나뿐이다.addTestCase(글 + 컴포넌트 id) · importCase(이미 있는 페이로드 폴더). 갈라지는 것은 replaced.json 을 만드느냐 하나뿐이고, 렌더부터는 같은 길이다.shrinkConfigs 로 폰트를 {min:1,max:1} 에 잠근다. 못 담을 글이 「작아진 것」이 아니라 「넘친 것」으로 남아야 재배치가 고칠 것을 찾는다.tight/*·outline.json 이 없으면 판정 박스가 없어 넘침이 0 으로 잡힌다 — 가장 망가진 케이스가 SUCCESS 로 떨어진다(5628689 실측).expected_result.json 은 대치 결과에서 기계적으로 안 나온다. 규칙 빌더가 만들고, 채점과 같은 잣대로 다시 재서 못 넘으면 지운다.verdict·problems 는 기계가 채운다. 손으로 쓰는 것은 note 하나다.브랜치 feat/post-replace-relayout · 커밋 9f1dfef7 의 작업트리다.
batch/pruneCases.ts 와 scripts/prune-test-cases.ts 두 파일에 미커밋 변경이 있고
(git diff --stat HEAD), 이 커밋은 원격에 없다 — GitHub permalink 가 404가 되므로
코드 링크는 vscode://file/… 편집기 링크로 건다.
숫자는 2026-08-28 이 저장소의 eval-out/cases 실측이다. 문서
(docs/*.md)가 더 큰 코퍼스(119건·137건·1,177건)를 인용하는 자리가 있는데, 그건 다른 시점·다른
브랜치의 값이다. 어긋나면 여기 적힌 실측이 지금의 값이다.
뷰어(PostReplaceRelayout.tsx)는 eval-out/cases/*/case.json 을
import.meta.glob 으로 읽는다. 따로 등록하는 단계가 없다 — 폴더가 생기면 화면이 바로 집는다.
그래서 「케이스를 만든다」는 곧 이 폴더를 채운다는 뜻이고, 아래가 그 목록이다.
| 파일 | 무엇 | 누가 만드나 | 드는 비용 |
|---|---|---|---|
| page.md | 대치한 요약 글. 뷰어는 안 읽는다 — 사람용 | addTestCase / importCase | — |
| original.json | 대치 이전 원안 = 채점 기준 | DB 조회 / 받은 폴더 | — |
| replaced.json | 대치 요청 페이로드(ThumbnailLoad) | addTestCase 가 만든다 importCase 는 받는다 | Supabase |
| rendered.json | 대치·렌더가 끝난 문서. 오토핏 끔 = 재배치의 입력 | 두 함수 모두 — iframe 렌더 | 브라우저 |
| tight/*.json | 엔진이 잰 glyph extent = 채점의 판정 박스 | bakeTightBoxes | 브라우저 |
| outline.json | 배경 도형의 실루엣 — 컨테이너 모양 | bakeOutlines | 네트워크 |
| case.json | id·variant·page·verdict·problems·note | labelCase + 사람(note) | — |
| expected_result.json | 정답 — 후보가 겨눌 목표선 | expected-result-build / 사람 | 브라우저(검증) |
| *.webp | 눈으로 볼 때만. 뷰어는 안 읽는다(라이브 iframe) | compositeBytes | — |
replaced 는 「대치된」이 아니다replaced.json 안의 sheetJson 은
대치가 안 된 원본 그대로다 — 넣을 텍스트는 nodeTextMap 에 따로 실린다.
이름의 «replaced» 는 대치 요청이 담겼다는 뜻이다.
eval-out/ 은 원래 .gitignore 대상인데 이 브랜치에서만 제외를 풀어 추적한다.
재생성 가능한 산출물(eval-out/runs/)을 섞지 않는다 — 재배치 결과(After 장)는 애초에 파일이 없다.
뷰어가 화면에서 그때그때 돌린다.
두 함수가 하는 일은 딱 한 군데에서만 다르다 — 대치 페이로드
(replaced.json)를 만드느냐 받느냐. 그 뒤로는 같은 함수를 부르고 같은 파일을 쓴다.
두 입구 · 한 합류점
rendered.json 은
같은 렌더 설정으로 나온다. 받아 온 폴더에 rendered.json 이 들어 있어도
쓰지 않는다: 그건 제품 파이프라인이 오토핏을 켜고 낸 다른 문서다(§5).comp_signs 시그니처가 필요하다.
원래 .claude/commands/comp-thumbnail.md 의 수동 6단계였고, 그 절차를 레포 코드로 옮긴 것이다.pickRefcomponentId(= design_objects.id)와 componentKey
(= design_objects.uuid)는 1:1 이 아니다. 한 key 에 적재분이 여러 개 붙는다.
| 준 것 | 어떻게 되나 |
|---|---|
| 둘 다 없다 | 예외 — 하나는 반드시 준다 |
| 둘 다 줬다 | componentId 를 쓰고 경고를 찍는다 |
id 가 숫자가 아니다 | 예외 — 「uuid 라면 componentKey 로 준다」 |
key 가 uuid 모양이 아니다 | 예외 — 「숫자 id 라면 componentId 로 준다」 |
| key 하나 | resolveDesignObjectRef 가 어느 적재분을 골랐는지 stdout 에 찍는다 |
글의 구조 해시가 컴포넌트의 구조 해시와 맞아야 대치가 성립한다.
parseSummaryTree(markdown) 로 요약 트리를 만들고 buildSearchVariations 가
변형들을 편다. 매칭은 두 출처를 순서대로 본다.
matchSignatures — 두 출처, 하나의 실패
design_object_id 로 적재된 structure_hash 를 읽어
글의 변형 해시와 맞춰 본다. 맞은 것이 있으면 여기서 끝이다createCompSignatureBuilder() 가 design_object_sheet_json 과
structure_json 에서 시그니처를 그 자리에서 만든다. structure_json 이 null 이면 예외base 이고, 없으면 매칭된 첫 번째다.variantLabel라벨은 dump-sheet-json.ts·comp-replace-render.ts 와 같은 규칙으로 만든다.
네 조각을 + 로 잇고, 아무것도 없으면 base 다.
buildContentReplacement 의 두 번째 인자를 비워서 부른다
(relieveTopBias + ensureBgMargin, 여백 12). comp-match UI 기본값과 같아야
케이스가 그 화면에서 본 것과 같은 그림이 되기 때문이다.
overwrite 의 기본값이 false 인 이유rendered.json 이 이미 있으면 멈춘다. 사람이 손본 expected_result.* 와
구워 둔 tight/* 는 어느 경우에도 안 건드리지만, 입력이 조용히 바뀌면 그것들의 채점이 뜻을 잃는다.
「이미 있다」는 것은 누군가 그 자리에 정답·판정 박스·note 를 쌓아 뒀다는 뜻이다.
케이스 폴더 이름은 언제나 design_object_id(숫자) 다.
받은 폴더 이름은 그게 아닐 수 있으니 아래 순서로 찾고, 다 실패하면 멈춘다 —
틀린 폴더에 케이스를 만드는 것이 제일 나쁘다.
resolveCaseId() — §id 해석 순서
resolveDesignObjectRef 가 최신 적재분을 집는데, 코퍼스가 쓰는 적재분과 다른 id 가 나온다.
MOR-2018/TC 반입 준비에서 uuid 로만 아는 140 폴더 중 116건이 이미 코퍼스에 있는 컴포넌트였고,
적재분별로 sheetJson 을 대조하니 최신 적재분만 2건에서 내용이 달라 그 id 로 라벨하면 오라벨이었다.「이미 있으면 거부」는 파일 존재 검사라, 병렬로 돌리면 같은 id 를 가리키는
두 폴더가 둘 다 통과해 뒤엣것이 앞엣것을 덮는다. MOR-2018/TC 에서 실제로 6쌍이 그렇게 겹쳐 썼다.
그래서 importCases 는 배치를 돌리기 전에 모든 폴더의 id 를 순서대로 확정하고,
먼저 온 폴더가 그 자리를 선점한다.
어느 글을 케이스로 남길지는 사람이 고를 일이지 실행 순서가 정할 일이 아니다. 건너뛴 목록은 보고에 남는다.
--concurrency 기본값은 1 이고, 올리면 크롬이 죽는다importCase 는 폴더마다 렌더를 한 번 하고, 기본 렌더 포트는 그때마다 브라우저를 열고 닫는다.
4 로 올려 11 폴더를 돌렸더니 「Failed to open a new tab」·「browser has been closed」로 9건이 깨졌다(2026-08-26 실측).
올리려면 브라우저를 살려 두는 renderDoc 포트를 함께 넘긴다.
오토핏을 켜면 못 담을 글이 넘침이 아니라 축소로 박힌다. 5628689 실측: 원안 38.7px → 27.1px(= 70%)에 넘침 노드 0. 그 문서를 재배치에 넣으면 후보가 고칠 것을 못 찾고, 「가독성이 떨어지게 작아졌다」는 아무도 손대지 않는다. 그래서 축소를 끈 채 굽는다.
ThumbnailLoad.fontScaleRange 는 배포된 렌더러가 읽지 않는다공유 타입에는 fontScaleRange?: {min, max} 가 있고 주석도 default {min:0.7, max:1} 이지만,
배포된 preview 페이지가 메시지에서 읽는 필드는 shrinkConfigs 다. 번들의 수신부가 그렇게 적혀 있다.
5628689 로 실측 — 같은 페이로드로 필드만 바꿔 네 번 렌더했다.
| 보낸 것 | 나온 폰트 | 판정 |
|---|---|---|
| 안 보냄 | 27.07 | 원안의 0.700 — 기본 오토핏 |
fontScaleRange {min:1, max:1} | 27.07 | 안 바뀐다 |
fontScaleRange {min:0.4, max:1} | 27.07 | 안 바뀐다 |
shrinkConfigs font {min:1, max:1} | 38.67 | 원안 그대로 ← 우리가 쓰는 것 |
shrinkConfigs font {min:0.4, max:1} | 22.41 | 더 줄어든다 |
| 축 | 배포된 기본 range | step |
|---|---|---|
lineSpacing | {min: 1} | 0.05 |
letterSpacing | {min: 0} | 1 |
font | {min: 0.7, max: 1} | 0.98 |
{min:1, max:1} 로 잠그고 행간·자간은 배포된 기본값을 그대로 적어 보낸다
(안 보내면 축 목록 자체가 기본으로 돌아간다).96건의 rendered.json 을 original.json 과 노드 id 로 짝지어 fontSize 를 견주면
텍스트 745쌍 중 줄어든 것이 0개다 — 최소 폰트 배율이 전 케이스에서 1.000 이다
(2026-08-28 실측). 축소가 실제로 꺼져 있다는 뜻이고, 못 담은 글은 전부 넘침으로 남아 있다.
렌더까지 끝나도 케이스는 아직 채점할 수 없다. 채점이 쓰는 두 값이 문서에 없기 때문이다 — 하나는 엔진만 알고, 하나는 리소스 원본만 안다.
tight/<문서>.jsonTextBoundingBoxCalculator 만이 값을 정의하고,
그것은 실제 폰트를 적재한 브라우저에서만 돈다. headless Chrome 에 엔진을 올려 재고 결과를 커밋할 수 있는 파일로 남긴다
— 뷰어는 DB 도 브라우저도 없이 그 파일만 읽는다.rendered.json · expected_result.json · original.jsonok / empty(빈 텍스트) / failed(계산 예외)outline.jsonprops.shape.fill.resource.key)이고
실루엣은 리소스 SVG/webp 를 래스터화해 등고선을 따야 나온다. 무인증 공개 API 라 자격 증명은 필요 없다.뷰어가 브라우저에서 그때그때 만드는 문서라 파일이 없다. 그 장은 rendered.json 의
tight bounding box 를 물려받되 크기가 그대로인 노드만 가져다 쓴다
(carryTightBoxes). 크기가 달라진 노드는 줄바꿈이 달라졌을 수 있어 물려받지 않고 비운다 —
없는 값을 박스로 대신 채우면 한 표 안에 두 기준이 섞인다.
이것이 순서를 지켜야 하는 이유다. 판정 박스가 없으면 텍스트가 전부 「못 잰 노드」가 되고, 넘침을 세는 지표는 그 노드를 판정에서 빼므로 넘침이 0 으로 잡힌다.
| 5628689 를 재면 | 판정 | 까닭 |
|---|---|---|
tight/* 없이 | SUCCESS | 넘침 0 — 잰 적이 없어서 0 이다 |
| 구운 값으로 | FAIL | 넘침 3노드 54,486px² · 신규 겹침 5쌍 |
2026-08-28 실측 · batch/pruneCases.ts 머리말
그래서 케이스를 들인 직후 case.json 에 적힌
verdict·problems 는 못 믿는다 — 굽고 나서
label-test-cases.ts 로 다시 붙인다.
판정은 여태 사람이 따로 스크립트를 돌려야 나왔다. 케이스가 백 건을 넘고 계속 느는데
들일 때마다 손으로 확인하는 것은 곧 아무도 안 하는 일이 된다. 그래서
case.json 을 쓸 때 판정과 problems 를 같은 계산에서 채운다.
labelCase() — 케이스 폴더 하나 → 라벨 한 벌
{ label, reasons[] } — 렌더본이 재배치의 입력이자 채점 대상이라 판정은 하나다{ excessiveFontScale, box } — 무엇으로 쟀나. 없으면 이 값이 낡았는지 알 수 없다overflow · shrink · idle-margin — 셋 다 숫자 비교다problems 세 어휘와 그 잣대| 값 | 언제 붙나 | 상수 | 왜 그 잣대인가 |
|---|---|---|---|
overflow | before 넘침 면적 > 원안 넘침 면적 | — | 원안 자체에 디자이너가 의도한 오버행이 있다. 원안과의 차이가 대치의 책임이다 |
shrink | 최소 폰트 배율 < 0.995 | SHRUNK_BELOW | 「조금이라도 줄었나」를 묻는다. 1.0 이 아닌 것은 반올림·부동소수 잡음을 걸러내려고 |
idle-margin | 유휴 여백 ≥ 0.65 | IDLE_MARGIN_AT_LEAST | 절대값이다 — 사람이 본 것은 「늘었나」가 아니라 「남아 있나」 였다 |
idle-margin 이 절대값인 것도 실측 결과다손으로 라벨한 3건(5628689 · 5825135 · 6134703)의 유휴 여백은 0.672 · 0.920 · 0.778 인데 원안 대비 증가분은 +0.001 · +0.163 · +0.000 으로 일정하지 않다.
problems.overflow 와 판정 게이트는 잣대가 다르다판정은 넘침 노드 수를, problems 는 면적을 원안과 견준다.
맞추지 않은 것은 답하는 질문이 달라서다 — 판정은 「몇 개가 새로 삐져나왔나」이고
problems 는 「대치가 넘침을 얼마나 키웠나」다.
writeCaseLabel 은 이미 채워져 있는 problems 를
그대로 둔다(keepProblems 기본 true). 눈으로 보고 붙인 라벨이
기계값에 덮이면 그 판단이 있었다는 것조차 다음 사람이 알 수 없기 때문이다. 대가가 있다.
shrink 77건은 낡은 값이다96건의 rendered.json 을 지금 재면 줄어든 텍스트가 745쌍 중 0개다(§5).
그런데 case.json 의 problems 에는 shrink 가 77건 남아 있다.
verdict 는 2026-08-27 에 box: "tight" 로 전건 다시 구워졌는데
(96건 전부 그 날짜다) problems 만 그대로인 것은, writeCaseLabel 이
이미 값이 있으면 안 덮기 때문이다. 렌더본을 축소 끈 판으로 다시 구웠을 때
그 값이 함께 갱신될 길이 없었다.
label-test-cases.ts --overwrite-problems 를 명시적으로 준다.verdict.rule 처럼
problems 에도 출처를 적으면 이 낙차가 보인다.| 2026-08-28 · 96건 | 건수 | 메모 |
|---|---|---|
| 판정 FAIL | 85 | 넘침·신규 겹침·과도 축소 중 하나 이상 |
| 판정 ACCEPTABLE | 11 | 게이트는 넘었는데 원안만큼은 아니다 |
| 판정 SUCCESS | 0 | 프루너가 이미 뺐다(§9) |
problems: overflow | 21 | 라벨은 들일 때의 값이라 낡을 수 있다(위 실측) |
problems: shrink | 77 | |
problems: idle-margin | 58 | |
problems: [] | 3 | 기계가 채우므로 정말로 문제 없음 |
addTestCase 도 importCase 도 이 파일을 만들지 않고,
반환값 skipped 에 그 사실만 담아 호출자가 안내하게 한다.
새 케이스는 Expected 장이 빈 상태로 뜨는 것이 정상이다.
Expected 는 「정답」이라 대치 결과에서 유도되지 않는다. 판정 규칙은
before-after-rules.md §2(불변 조건)와 evaluation-rules.md §2(판정 지표)에 있고,
그 규칙대로 문서를 짜는 것은 사람 또는 규칙 빌더의 몫이다.
정답 만들기 — 네 걸음
--gate 가 아니라 ③ 인가 — 빌더의 --gate 는 답을 쓰기 전에 거르는데,
그때는 정답의 tight bounding box 가 없어 프레임을 끼워 넣는다. 프레임에는 오토핏 여백이 들어 있어 넘침·겹침을 과대 보고한다 —
2026-08-26 실측 111건에서 프레임으로 ⑤ 위반 70건 vs 구운 tight 로 55건, 즉 16건이 억울했다.
그래서 순서를 바꿨다: 일단 쓰고 → 굽고 → 같은 잣대로 재서 못 넘은 것만 지운다.③ 이 정답을 지우면 case.json 에
expectedResult: { status: "unsolvable", reason, at } 를 적는다.
그 표시가 있는 케이스는 판정에서 UNSOLVABLE 이고,
없으면 UNKNOWN 이다.
| 라벨 | 뜻 | 2026-08-28 · 96건 |
|---|---|---|
| UNSOLVABLE | 알아본 결과 규칙 안에서 답이 없다 | 51 |
| UNKNOWN | 아직 알아보지 못했다 — 통과로 뭉개지 않는다 | 45 (정답 파일 있음) |
정답 파일이 있는 45건에는 expectedResult 필드가 없다 — 지워진 적이 없다는 뜻이다.
Expected 열은 후보가 겨눌 과녁이라, 과녁이 틀리면 후보를 고칠 때마다 틀린 방향으로 끌려간다. ⑤ 신규 겹침이 1쌍이라도 있거나 §2.2 내접 이탈이 원안보다 늘면 지운다. 지운 자리는 「재배치로 못 고치는 것」이고 그것이 정답이다.
케이스를 들이는 것만큼 빼는 것도 제작 로직이다.
before 판정이 SUCCESS 인 케이스는 대치가 아무것도 망가뜨리지 않아
재배치가 고칠 것이 없다 — 어떤 후보를 넣어도 점수가 안 움직이므로 표만 길어지고
후보 사이의 차이를 희석한다.
pruneCases() — 두 갈래로 뺀다
UNKNOWN 은 뺄 수 없다 — 판정할 재료가 없다는 뜻이라 지울 근거도 없고,
pruneVerdicts 에 넣으면 예외로 던진다. keepWithExpected: true 를 주면
expected_result.json 이 있는 케이스는 지우지 않는다(기본은 함께 지우고, 지운 것 중 정답이 있던 건은
prunedWithExpected 로 따로 세서 조용히 사라지지 않게 한다).뷰어는 case.json 으로 케이스를 세므로, 반쯤 지워 case.json 만 남으면
그 케이스가 계속 목록에 오른다. 그래서 eval-out/cases/<id>/ 를 통째로 지운다 —
되돌리려면 다시 들여야 한다. dryRun 이 기본이 아닌 이유가 없다:
호출자가 먼저 --dry-run 으로 목록을 보게 하는 것이 이 모듈의 사용법이다.
tsx 는 레포의 전이 의존성으로만 있어 pnpm exec tsx 가 안 먹는다.
실행기를 먼저 잡는다.
폴더 하나 → 채점 가능한 케이스
original·replaced·rendered·page.md·case.json 이 생긴다. --skip-existing 을 늘 붙인다tight/rendered.json · tight/original.jsonoutline.json. 없으면 캡슐·원·말풍선의 넘침이 실제보다 작게 잡힌다verdict·problems 는 판정 박스가 없던 시점의 값이라 못 믿는다(§6)note 쓰기note 하나. problems 는 눈으로 본 판단이 기계값과 다를 때만 고친다.env.local)은 addTestCase 와 case id 를 uuid 로 해석할 때만 읽는다.--overwrite 로 밀지 않는다겹치면 새로 넣지 않고 건너뛴다. 케이스가 이미 있다는 것은 누군가 그 자리에 정답·판정 박스·note 를 쌓아 뒀다는 뜻이고, 입력을 조용히 갈아치우면 그 위의 채점이 전부 뜻을 잃는다. 겹치는 길이 둘이다 — 코퍼스에 이미 있거나(폴더 존재), 한 배치 안에서 두 폴더가 같은 id 로 가거나 (MOR-2018/TC 에 7조 15폴더). 둘 다 건너뛴다.
prune-test-cases.ts 를 돌리지 않는다안 구운 케이스는 넘침이 0 으로 잡혀 SUCCESS 로 떨어지고 통째로 지워진다.
지금은 프루너가 안 구운 폴더를 UNKNOWN 으로 남기고 막지만(--allow-unbaked 로만 밀린다),
순서를 지키는 것이 먼저다.
rendered.json 을 쓰지 않는다제품 파이프라인이 오토핏을 켜고 낸 문서라 글자가 이미 줄어 있다. 렌더본은 언제나 여기서 축소를 끄고 새로 낸다 — 그래서 폴더마다 브라우저를 한 번은 쓴다. 「폴더만 읽고 끝나는 갈래」는 없다.
problems 는 저절로 안 낡는다 — 낡은 채로 남는다keepProblems 기본값이 사람 판단을 지키느라 기계값도 함께 지킨다.
렌더본을 다시 굽거나 판정 규칙을 바꿨으면 --overwrite-problems 를 명시적으로 준다
(§7 실측: 지금 shrink 77건이 그 상태다).
webp 를 눈으로 본다이 경로는 검색(comp-match)의 글자 용량 하한(cap_chars/cap_lines) 필터를 건너뛰므로,
구조 해시가 맞았다는 것이 들어갈 자리가 넉넉하다는 뜻은 아니다 — 넘침·과축소가 흔하다.
특히 정답은 지표가 「넘침 0」이라도 그림에서 삐져나올 수 있다
(ShapeItem 중에는 boundingBox 만 키워도 렌더 크기가 안 따라오는 것이 있다, 6869327 실측).
| 무엇 | 파일 | 진입점 |
|---|---|---|
| 글 + 컴포넌트 → 케이스 | batch/addTestCase.ts:327 | addTestCase |
| 페이로드 폴더 → 케이스 | batch/importCase.ts:290 | importCase · importCases |
| case id 해석(§id) | batch/importCase.ts:208 | resolveCaseId |
| 축소 끄는 레버 | batch/renderShrink.ts:65 | NO_FONT_SHRINK · withoutFontShrink |
| 판정 박스 굽기 | batch/bakeTightBoxes.ts:89 | bakeTightBoxes |
| 컨테이너 모양 굽기 | batch/bakeOutlines.ts:121 | bakeOutlines |
| 구운 값 읽기 | batch/caseTight.ts · caseOutline.ts | readCaseTight · readCaseOutlines |
| 라벨 붙이기 | batch/labelCases.ts:122 | labelCase · problemsOf · labelCases |
| 코퍼스 정리 | batch/pruneCases.ts:242 미커밋 | pruneCases · idleOnlyGrowth |
| 렌더 재검증 포트 | batch/tightBoxProbe.ts | createTightBoxProbe |
batch/ 아래는 브라우저가 아니라 Node 에서 도는 코드다
(src/text-tight-box/batch·src/resource-outline/batch 와 같은 규약). 뷰어 쪽 코드는 이 폴더를 import 하지 않는다
— 브라우저는 디스크에 못 쓰니 파일을 만드는 일은 애초에 Node 몫이다. CLI 는 scripts/ 의 껍데기이고
순서와 계약은 전부 모듈에 있다.
여기서 만든 케이스를 무엇으로 어떻게 채점하는지는
알고리즘 적용 결과 채점 로직 에 있다 —
판정 박스 셋, 지표 ①~⑦, 그리고 SUCCESS/ACCEPTABLE/FAIL 게이트.