diff --git a/5_BURN_STRIP.md b/5_BURN_STRIP.md
new file mode 100644
index 0000000..a6cd99f
--- /dev/null
+++ b/5_BURN_STRIP.md
@@ -0,0 +1,264 @@
+# 5 — Đốt dải thông tin ("strip") vào ảnh xuất
+
+Nhánh: `feat/vision-camera-v5`. Ngày: 2026-09-06.
+Nguồn đối chiếu: nhánh `recipes-web`, commit **`b4d5d29`** — bản web đã làm và đo được.
+
+---
+
+## 1. Mục tiêu
+
+Ảnh **xuất ra** phải mang đúng dải thông tin mà app đang hiển thị trên thẻ recipe:
+một **tagline amber** `#TAG` nằm góc trên-trái **trên chính bức ảnh**, và một **dải
+caption nền tối** treo **bên dưới** ảnh, gồm **tên recipe** (đậm, sáng) và **dòng
+kỹ thuật** `ISO · GRAIN · WARMTH/MONO` (mono, xám).
+
+Đây là bản sao của khối `.lp-shot` + `.lp-frame-meta` trên landing:
+
+```html
+
+
![]()
+
#ACROS_100
+
+
+ ACROS 100
+ ISO 400 · GRAIN 4 · WARMTH +2
+
+```
+
+Thuộc tính web (để đối chiếu màu/kích thước): `docker/frontend/src/styles/landing.css`
+dòng 288–291 — tagline `color: var(--lp-amber)`, meta `font-family: var(--lp-mono)`.
+
+**Ranh giới quan trọng:** dải này CHỈ có trong **file xuất**. Preview / viewfinder /
+màn hình chỉnh sửa **không** được vẽ nó — nếu không, người dùng thấy hai lần.
+
+---
+
+## 2. Giá trị lấy từ đâu
+
+Bản web tính trong một `useMemo` duy nhất (`recipes-web` @ `b4d5d29`,
+`docker/frontend/src/App.tsx`, dòng ~658–671):
+
+```ts
+const name = (recipe.name || 'RECIPE').trim();
+const slug = name.replace(/[^\p{L}\p{N}]+/gu, '_').replace(/^_+|_+$/g, '').toUpperCase();
+const grain = Math.max(0, Math.round(recipe.adjustments.grain ?? 0));
+const warmth = Math.round((recipe.adjustments.temperature - 5500) / 250);
+const tail = recipe.baseFilter === 'monochrome'
+ ? 'MONO'
+ : `WARMTH ${warmth >= 0 ? '+' : ''}${warmth}`;
+const strip = {
+ tag: `#${slug || 'RECIPE'}`, // UPPERCASE, khoảng trắng -> '_'
+ title: name, // giữ nguyên hoa/thường
+ meta: `ISO ${iso ?? 'AUTO'} · GRAIN ${grain} · ${tail}`,
+};
+```
+
+- `·` là **U+00B7 MIDDLE DOT**, có dấu cách hai bên.
+- `iso` là ISO đọc từ **EXIF của ảnh nguồn**; không có thì in `AUTO` (xem §5).
+- `grain` lấy thẳng từ knob `adjustments.grain` (0..10), **không** cộng phần re-grain
+ của NOISE REDUCTION âm — dòng kỹ thuật mô tả *công thức*, không mô tả pass xuất.
+- `warmth` là bước nhảy 250K quanh mốc 5500K, luôn kèm dấu (`+2` / `-3`).
+- `baseFilter === 'monochrome'` thì thay hẳn `WARMTH …` bằng `MONO`.
+
+---
+
+## 3. Hình học & màu (chính xác từng pixel)
+
+Đặt `W`, `H` = chiều rộng/cao của ảnh **sau khi đã ghép frame** (ảnh export cuối cùng,
+trước encode). Mọi kích thước suy từ `W`:
+
+| Đại lượng | Công thức |
+|---|---|
+| `band` | `round(W * 0.155)` |
+| `pad` | `round(W * 0.03)` |
+| `tagSize` | `round(W * 0.026)` |
+| `titleSize` | `round(W * 0.042)` |
+| `metaSize` | `round(W * 0.026)` |
+
+Các bước vẽ, **theo đúng thứ tự**:
+
+1. Tạo surface mới `(W, H + band)` — **ảnh cao thêm đúng `band`**. Vẽ ảnh cũ vào `(0, 0)`.
+2. **Tagline** trên ảnh, góc trên-trái, font mono:
+ - bản `#000000` tại `(pad + 1, pad + tagSize + 1)` (drop shadow, để tag đọc được
+ trên trời sáng),
+ - bản `#f59e0b` (amber) tại `(pad, pad + tagSize)`.
+3. **Dải nền** dưới ảnh: hình chữ nhật `(0, H, W, band)` tô `#0b0b0b`.
+4. **Tên recipe** `#f2f2f2`, baseline `(pad, H + pad + titleSize)`.
+5. **Dòng kỹ thuật** `#9a9a9a`, baseline
+ `(pad, H + pad + titleSize + round(metaSize * 1.7))` — tức cách baseline tên
+ `round(metaSize * 1.7)`.
+
+Font: **Cousine Regular** — cùng typeface app đã bundle
+(`assets/Cousine-Regular.ttf`; engine đã nạp sẵn trong `watermarkFonts.typeface`).
+
+Ví dụ đo được (web): nguồn 1200×900 → file xuất **1200×1086** (`band = 186`).
+
+---
+
+## 4. Ba đường phải sửa (parity gate)
+
+Bản web chỉ có một engine; bản app có **ba** đường vẽ, cả ba phải ra **cùng một file**:
+
+### 4.1 `src/utils/exportEngine.ts` — engine Skia (bước 9b)
+
+Chèn ngay **trước** dòng encode (`let bytes = resultImage.encodeToBytes(...)`, ~dòng 917),
+sau bước snapshot + sharpen (bước 9 hiện tại, ~dòng 896):
+
+```ts
+const caption = options?.caption ?? null; // thêm vào ExportOptions
+if (caption) {
+ const W = resultImage.width();
+ const band = Math.round(W * 0.155);
+ const capSurface = createSurface(W, resultImage.height() + band);
+ const loaded = watermarkFonts; // cùng typeface Cousine đã nạp
+ if (capSurface && loaded) {
+ own(capSurface);
+ const cc = capSurface.getCanvas();
+ cc.drawImage(resultImage, 0, 0, own(Skia.Paint()));
+
+ const pad = Math.round(W * 0.03);
+ const tagSize = Math.round(W * 0.026);
+ const tagFont = own(Skia.Font(loaded.typeface, tagSize));
+ const shadow = own(Skia.Paint()); shadow.setColor(Skia.Color('#000000'));
+ const amber = own(Skia.Paint()); amber.setColor(Skia.Color('#f59e0b'));
+ cc.drawText(caption.tag, pad + 1, pad + tagSize + 1, shadow, tagFont);
+ cc.drawText(caption.tag, pad, pad + tagSize, amber, tagFont);
+
+ const bandTop = resultImage.height();
+ const bandPaint = own(Skia.Paint()); bandPaint.setColor(Skia.Color('#0b0b0b'));
+ cc.drawRect(Skia.XYWHRect(0, bandTop, W, band), bandPaint);
+ const titleSize = Math.round(W * 0.042);
+ const metaSize = Math.round(W * 0.026);
+ const titleFont = own(Skia.Font(loaded.typeface, titleSize));
+ const metaFont = own(Skia.Font(loaded.typeface, metaSize));
+ const titlePaint = own(Skia.Paint()); titlePaint.setColor(Skia.Color('#f2f2f2'));
+ const metaPaint = own(Skia.Paint()); metaPaint.setColor(Skia.Color('#9a9a9a'));
+ cc.drawText(caption.title, pad, bandTop + pad + titleSize, titlePaint, titleFont);
+ cc.drawText(caption.meta, pad, bandTop + pad + titleSize + Math.round(metaSize * 1.7),
+ metaPaint, metaFont);
+
+ const composed = own(capSurface.makeImageSnapshot());
+ release(owned, resultImage); // đăng ký `own`/`release` như mọi buffer khác
+ resultImage = composed;
+ } else if (capSurface) {
+ capSurface.dispose();
+ }
+}
+```
+
+`ExportOptions` thêm:
+
+```ts
+// Dải file mang theo: tagline amber trên ảnh + caption band bên dưới.
+// Để trống cho preview/thumbnail — chỉ đường xuất truyền vào.
+caption?: { tag: string; title: string; meta: string } | null;
+```
+
+Và tại call-site xuất (App.tsx) truyền `caption: strip` (đúng object §2) — **không**
+truyền vào đường preview.
+
+### 4.2 `src/utils/nativeExport.ts` + `RecipescamExportModule.kt` — đường native
+
+Đường native không đi qua Skia, nên phải **vẽ band trong Kotlin**. Thêm `caption`
+vào `NativeExportOptions` và đẩy xuống map `adjust` (cùng chỗ với `geo`, `frameId`):
+
+```ts
+caption: options?.caption ?? null, // tag/title/meta — null khi không xuất file
+```
+
+Trong `RecipescamExportModule.kt`, chèn **giữa bước 7 (sharpen) và bước 8 (encode)**
+(~dòng 568, sau khối `if (sharpen > 0) { … }`), vẽ lên chính `render`:
+
+```kotlin
+// 7b. Strip (parity với exportEngine #9b): band mới cao hơn ảnh.
+val cap = adjust["caption"] as? Map<*, *>
+if (cap != null) {
+ val tag = cap["tag"] as? String
+ val title = cap["title"] as? String
+ val meta = cap["meta"] as? String
+ if (tag != null && title != null && meta != null) {
+ val W = render.width; val H = render.height
+ val band = Math.round(W * 0.155f)
+ val taller = Bitmap.createBitmap(W, H + band, Bitmap.Config.ARGB_8888)
+ val cc = Canvas(taller)
+ cc.drawBitmap(render, 0f, 0f, null)
+ val pad = Math.round(W * 0.03f)
+ val tagSize = Math.round(W * 0.026f)
+ val titleSize = Math.round(W * 0.042f)
+ val metaSize = Math.round(W * 0.026f)
+ // Cùng typeface Cousine mà stampGeo đang dùng (raw assets_cousineregular).
+ val face = geoTypefaceRes(ctx, "assets_cousineregular")
+ // tagline: shadow đen lệch 1px rồi amber
+ val shadow = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.BLACK; typeface = face; textSize = tagSize.toFloat() }
+ val amber = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor("#f59e0b"); typeface = face; textSize = tagSize.toFloat() }
+ cc.drawText(tag, (pad + 1).toFloat(), (pad + tagSize + 1).toFloat(), shadow)
+ cc.drawText(tag, pad.toFloat(), (pad + tagSize).toFloat(), amber)
+ // dải nền tối
+ cc.drawRect(0f, H.toFloat(), W.toFloat(), (H + band).toFloat(),
+ Paint().apply { color = Color.parseColor("#0b0b0b") })
+ val titlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor("#f2f2f2"); typeface = face; textSize = titleSize.toFloat() }
+ val metaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor("#9a9a9a"); typeface = face; textSize = metaSize.toFloat() }
+ cc.drawText(title, pad.toFloat(), (H + pad + titleSize).toFloat(), titlePaint)
+ cc.drawText(meta, pad.toFloat(), (H + pad + titleSize + Math.round(metaSize * 1.7)).toFloat(), metaPaint)
+ render.recycle()
+ render = taller
+ }
+}
+```
+
+Lưu ý: band nằm **dưới frame** — RETRO POLAROID / WALL FRAME đã ghép xong trước đó
+(bước 6b), nên `H` lúc này là chiều cao tấm card/khung, không phải ảnh gốc. Web làm
+y hệt: bước 9b chạy sau khi frame đã ghép.
+
+### 4.3 `src/components/Viewfinder.tsx` — preview
+
+**Không** vẽ gì. Đây là điểm dễ sai nhất: dải thuộc về *file*, không thuộc về màn hình.
+
+---
+
+## 5. ISO từ EXIF
+
+Bản web: `exifr.parse(bytes, { pick: ['ISO', 'ISOSpeedRatings'] })` →
+`number | null`; `null` in `AUTO` (`recipes-web` @ `b4d5d29`,
+`docker/frontend/src/engine/imageOps.ts`).
+
+App hiện **chưa đọc tag ISO** (chỉ có GPS trong `src/utils/exifGps.ts` và ghi EXIF
+trong `exifWrite.ts`). Thêm một helper nhỏ cạnh `exifGps.ts` đọc APP1/TIFF tag
+`0x8827` (ISOSpeedRatings) từ bytes ảnh nguồn, hoặc lấy từ EXIF mà asset của ảnh đã
+nạp sẵn có. Chưa có thì trả `null` → dòng kỹ thuật in `ISO AUTO`.
+
+Đừng bịa ISO từ knob: build này `ISO_SUPPORTED = false`
+(`src/components/RecipeCreateModal.tsx` dòng 74) — ISO trong dòng kỹ thuật là ISO
+**của máy ảnh lúc chụp**, không phải một lựa chọn trong app.
+
+---
+
+## 6. Kiểm thử (bắt buộc trước khi coi là xong)
+
+1. Xuất ảnh có recipe đặt tên, grain > 0, temperature ≠ 5500.
+2. Kỳ vọng hình học: file cao hơn ảnh gốc đúng `round(W * 0.155)`; ví dụ nguồn
+ 1200×900 → **1200×1086**.
+3. Đo pixel: hàng trong dải nền tối (độ sáng < 0.2); **tagline amber** có pixel
+ `#f59e0b` (đếm > 20 pixel) ở góc trên-trái *trên ảnh*; chữ `#f2f2f2` /
+ `#9a9a9a` nằm trong dải dưới.
+4. Chạy **cả ba đường** — engine Skia, native Kotlin, và (đối chiếu) web @ `b4d5d29`.
+ Band phải trùng khít giữa ba đường: cùng chiều cao, cùng màu, cùng baseline.
+5. Preview / viewfinder: khẳng định **không** xuất hiện dải (không có pixel amber
+ `#f59e0b` ngoài watermark vốn có).
+6. Đối chiếu web: `docker/frontend/src/engine/exportEngine.ts` bước **9b**
+ (`recipes-web` @ `b4d5d29`).
+
+---
+
+## 7. Ghi chú phụ
+
+- Band đặt **sau** frame và **trước** encode — nếu vẽ trước frame, khung sẽ đè mất
+ chữ; nếu vẽ sau encode, file không còn pixel để vẽ.
+- `band` tính từ **chiều rộng ảnh cuối**, không phải chiều rộng màn hình: ảnh 12MP
+ và ảnh 1200px ra tỉ lệ chữ khác nhau, đúng như mong đợi.
+- Khi ảnh đã có watermark người dùng / GPS stamp, dải vẫn treo ngoài ảnh — không
+ chồng lên các lớp đó.
+- Nếu sau này app có "lưu vào bộ sưu tập" tách khỏi "tải xuống": bản web cố ý lưu
+ bản **sạch** (không đốt dải) cho lưới cộng đồng và chỉ đốt dải vào file tải xuống,
+ để khung của lưới tự vẽ tagline từ nhãn đã lưu — tránh tag hai lần. Nếu app không
+ có lưới cộng đồng thì cứ đốt thẳng vào file xuất.