<!doctype html>
<html lang="en">
<head><base href="about:srcdoc"><script>(function(){function n(){parent.postMessage({t:'share:hash',h:location.hash},'*')}addEventListener('hashchange',n);addEventListener('message',function(e){if(e.source!==parent)return;var d=e.data||{};if(d.t==='share:setHash'&&typeof d.h==='string'&&d.h!==location.hash){location.hash=d.h}});addEventListener('DOMContentLoaded',function(){parent.postMessage({t:'share:ready',h:location.hash},'*')});})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ePhyto Hub — Two decisions for BAFRA</title>
<style>
:root{--bg:#f7f9fc;--paper:#fff;--ink:#122033;--muted:#536173;--line:#d9e1ea;--accent:#0b5cab;--soft:#eef5fc;--ok:#0f7a3a;--warn:#a45c00;--ok-bg:#e8f5ee;--warn-bg:#fff8ee}
*{box-sizing:border-box}
body{margin:0;background:linear-gradient(180deg,#f3f7fb,#eef4f9 35%,#f7f9fc);color:var(--ink);font:17px/1.55 "Segoe UI",Arial,sans-serif}
.wrap{max-width:820px;margin:0 auto;padding:32px 24px 56px}
.hero,.card{background:var(--paper);border:1px solid var(--line);border-radius:20px;box-shadow:0 14px 42px rgba(18,32,51,.06)}
.hero{padding:28px 30px 22px;margin-bottom:18px}
.eyebrow{margin:0 0 8px;color:var(--accent);font:700 12px/1.2 Arial,sans-serif;letter-spacing:.12em;text-transform:uppercase}
h1,h2{font-family:Georgia,"Times New Roman",serif}
h1{margin:0 0 12px;font-size:32px;line-height:1.1}
h2{margin:0 0 12px;font-size:22px}
.lede{margin:0;color:var(--muted)}
.card{padding:22px 26px;margin-bottom:14px}
.card summary{cursor:pointer;list-style:none;display:flex;align-items:baseline;gap:14px;padding:4px 0}
.card summary::-webkit-details-marker{display:none}
.num{display:inline-block;min-width:34px;height:34px;line-height:34px;text-align:center;border-radius:50%;background:var(--soft);color:var(--accent);font-weight:700;font-size:14px;border:1px solid #d7e5f4}
.title{font-family:Georgia,serif;font-size:20px;font-weight:400;flex:1}
details[open] summary .title{color:var(--accent)}
.body{padding:14px 0 4px 48px}
.body p{margin:0 0 10px}
.reply{margin-top:16px;padding:14px 16px;background:#fafbfd;border:1px dashed #cfd8e3;border-radius:10px}
.reply.filled{background:var(--ok-bg);border-color:#b7d9c2;border-style:solid}
.reply-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.reply-label{font-size:12px;font-weight:700;color:var(--accent);letter-spacing:.06em;text-transform:uppercase}
.clearOne{background:none;border:none;color:var(--muted);font-size:12px;cursor:pointer;padding:2px 6px;border-radius:5px}
.clearOne:hover{color:var(--warn);background:#fff4e5}
.reply-opts{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}
.reply-opts label{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;border:1px solid var(--line);background:#fff;cursor:pointer;font-size:14px}
.reply-opts label:has(input:checked){background:var(--soft);border-color:var(--accent)}
.reply-opts input[type=radio]{margin:0}
.reply textarea{width:100%;min-height:60px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;font:14px/1.45 "Segoe UI",Arial,sans-serif;resize:vertical;color:var(--ink);background:#fff}
.reply textarea:focus{outline:none;border-color:var(--accent)}
.statusbar{position:sticky;top:0;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:14px;padding:10px 14px;margin-bottom:14px;display:flex;justify-content:space-between;align-items:center;font-size:14px;box-shadow:0 6px 20px rgba(18,32,51,.08)}
.statusbar .progress{color:var(--accent);font-weight:600}
.statusbar .saveMark{color:var(--ok);font-size:12px}
.submit{background:var(--paper);border:1px solid var(--line);border-radius:20px;padding:22px 26px;margin-top:24px;text-align:center;box-shadow:0 14px 42px rgba(18,32,51,.06)}
.submit h2{margin-top:0}
.submit p{margin:0 0 14px;color:var(--muted);font-size:15px}
.btnrow{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:10px}
.btn{display:inline-flex;align-items:center;gap:6px;padding:11px 20px;border-radius:10px;border:1px solid var(--accent);background:var(--accent);color:#fff;font:600 14px "Segoe UI",Arial,sans-serif;cursor:pointer;text-decoration:none}
.btn:hover{background:#094a8a;border-color:#094a8a}
.btn.secondary{background:#fff;color:var(--accent)}
.btn.secondary:hover{background:var(--soft);color:#094a8a}
.btn.ghost{background:#fff;color:var(--muted);border-color:var(--line)}
.btn.ghost:hover{background:#fff4e5;color:var(--warn);border-color:#f0d9b5}
.status{min-height:22px;color:var(--ok);font-weight:600;font-size:14px;margin-top:6px}
.preview{margin-top:14px;text-align:left;max-height:280px;overflow:auto;padding:14px;background:#f8faff;border:1px solid var(--line);border-radius:10px;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;color:#2a3b52;display:none}
.preview.show{display:block}
.note{margin-top:18px;padding:14px 16px;background:var(--soft);border:1px solid #cfe0f1;border-radius:10px;font-size:14px;color:var(--ink)}
.note strong{color:var(--accent)}
.note ul{margin:6px 0 0 0;padding-left:20px}
.note li{margin:2px 0}
a{color:var(--accent)} a:hover{text-decoration:underline}
@media(max-width:720px){.wrap{padding:18px 14px 40px}.hero,.card,.submit{border-radius:16px;padding:18px}.body{padding-left:0}}
</style>
</head>
<body>
<div class="wrap">
<div class="statusbar">
<span class="progress" id="progress">0 of 2 items answered</span>
<span class="saveMark" id="saveMark">Replies are saved in your browser as you type</span>
</div>
<section class="hero">
<p class="eyebrow">For Tshering & Pramod • Please share with BAFRA</p>
<h1>Two decisions to send Bhutan certificates to the IPPC Hub</h1>
<p class="lede">
After studying the Hub rules in detail, we only need two answers from BAFRA. Everything else our software will handle automatically — no form changes for officers.
</p>
</section>
<details class="card" open>
<summary><span class="num">1</span><span class="title">Attach international codes to the “Importing country” list</span></summary>
<div class="body">
<p><strong>What we do:</strong> Behind the scenes, we attach the two-letter international code (BD, US, IN…) to each country in your existing dropdown.</p>
<p><strong>What officers see:</strong> Nothing. Same dropdown, same labels.</p>
<p><strong>Why:</strong> The Hub will only accept country names sent as the standard two-letter code. This is the only place where the Hub strictly enforces a code list.</p>
<div class="reply" data-item="p1" data-title="1. Attach international codes to the Importing country list">
<div class="reply-head">
<span class="reply-label">Your decision</span>
<button type="button" class="clearOne">Clear this answer</button>
</div>
<div class="reply-opts">
<label><input type="radio" name="p1" value="please-do"><span>Please do it</span></label>
<label><input type="radio" name="p1" value="we-do"><span>We’ll do it</span></label>
<label><input type="radio" name="p1" value="skip"><span>Skip</span></label>
</div>
<textarea placeholder="Comments or questions"></textarea>
</div>
</div>
</details>
<details class="card" open style="border-color:#f0d9b5;background:var(--warn-bg)">
<summary><span class="num" style="background:#fff4e5;color:var(--warn);border-color:#f0d9b5">2</span><span class="title">Who signs the certificate — Inspector or OIC?</span></summary>
<div class="body">
<p>The Hub certificate carries one signatory name. The service has two candidate fields: the <em>Inspector</em> and the <em>OIC (Officer in Charge)</em>. Tell us which name to put on the certificate.</p>
<div class="reply" data-item="qA" data-title="2. Who signs — Inspector or OIC?">
<div class="reply-head">
<span class="reply-label">Your answer</span>
<button type="button" class="clearOne">Clear this answer</button>
</div>
<div class="reply-opts">
<label><input type="radio" name="qA" value="inspector"><span>Inspector</span></label>
<label><input type="radio" name="qA" value="oic"><span>OIC</span></label>
<label><input type="radio" name="qA" value="other"><span>Other (see comment)</span></label>
</div>
<textarea placeholder="Optional comment"></textarea>
</div>
</div>
</details>
<details class="card" open>
<summary><span class="num">→</span><span class="title">What happens next, on our side</span></summary>
<div class="body">
<p>Once you answer these two questions, we build a small piece of software that takes each approved certificate and sends it to the IPPC Hub automatically. No extra clicks for inspectors — the existing workflow simply gains one more step at the end.</p>
<p>If you'd like to see what we're building and in what order: <a href="https://share.eregistrations.dev/d/c-uH0UnLvJ">the implementation plan, with a diagram</a>.</p>
</div>
</details>
<details class="card">
<summary><span class="num">i</span><span class="title">Why this list is shorter than the previous one</span></summary>
<div class="body">
<p>
An earlier version of this doc asked BAFRA to decide on six form changes. After reading the official Hub rules in detail (IPPC ISPM 12, ePhyto Mapping Guide v2.12, Hub Web Service API v1.20), we confirmed that the Hub does <strong>not</strong> check certificate content — it only checks the envelope and basic XML structure. So we can keep the existing form and let our bridge software do the conversion.
</p>
<p>What we now handle in software, with no impact on BAFRA officers:</p>
<ul>
<li>Treatment textbox → sent to the Hub as full free text (the Hub allows this).</li>
<li>Additional declarations textbox → sent to the Hub as one declaration block, officer's text verbatim (up to 8,000 characters; Hub does not require coded entries).</li>
<li>Unit dropdown → we maintain a small translation table on our side: recognised units (kg, m³, tonnes) go to the structured quantity slot, package types (boxes, bags, drums) go to a free-text package note. Unknown entries fall back to free text.</li>
<li>Import permit number → only sent when present; no new field needed.</li>
<li>Transit countries → only sent when present; no new field needed.</li>
<li>Other catalogs (commodities, ports, transport, etc.) → IPPC publishes recommended code lists, but the Hub does not enforce them. We send Bhutan's existing values as-is. Risk: a destination country's software may silently mark our certificates as non-conformant. If that happens we map that specific catalog.</li>
<li>Exit point → stays in the form for internal use; the Hub only expects the destination port.</li>
</ul>
<p>Full reasoning with source citations: <a href="https://share.eregistrations.dev/d/0ca1akRQ8Z">share.eregistrations.dev/d/0ca1akRQ8Z</a>.</p>
</div>
</details>
<section class="submit">
<h2>Send your answers</h2>
<p>Your answers are saved in this browser. To share them with us, pick one:</p>
<div class="btnrow">
<button class="btn" type="button" id="downloadBtn">Download replies (JSON)</button>
<a class="btn secondary" id="mailBtn" href="#">Open email with my replies</a>
<button class="btn secondary" type="button" id="copyBtn">Copy to clipboard</button>
</div>
<div class="btnrow" style="margin-top:4px">
<label class="btn ghost" for="uploadInput" style="cursor:pointer">Upload previous replies (JSON)</label>
<input type="file" id="uploadInput" accept="application/json,.json" style="display:none">
<button class="btn ghost" type="button" id="previewBtn">Preview the text</button>
<button class="btn ghost" type="button" id="clearAllBtn">Clear all answers</button>
</div>
<div class="status" id="status"></div>
<div class="preview" id="preview"></div>
</section>
</div>
<script>
const STORAGE_KEY = 'bafra-ephyto-replies-v2';
const decisionLabels = {
'we-do': "We'll do it",
'please-do': "Please do it",
'skip': "Skip",
'inspector': "Inspector",
'oic': "OIC",
'other': "Other (see comment)"
};
const statusEl = document.getElementById('status');
const previewEl = document.getElementById('preview');
const progressEl = document.getElementById('progress');
const saveMarkEl = document.getElementById('saveMark');
function replyWidgets() { return document.querySelectorAll('.reply'); }
function readState() {
const state = {};
replyWidgets().forEach(b => {
const name = b.dataset.item;
const checked = b.querySelector(`input[name="${name}"]:checked`);
const comment = b.querySelector('textarea').value;
state[name] = { decision: checked ? checked.value : null, comment };
});
return state;
}
function applyState(state) {
for (const [name, r] of Object.entries(state || {})) {
const widget = document.querySelector(`.reply[data-item="${name}"]`);
if (!widget) continue;
widget.querySelectorAll(`input[name="${name}"]`).forEach(x => x.checked = false);
if (r && r.decision) {
const radio = widget.querySelector(`input[name="${name}"][value="${r.decision}"]`);
if (radio) radio.checked = true;
}
const ta = widget.querySelector('textarea');
if (ta) ta.value = (r && r.comment) || '';
}
refresh();
}
function refresh() {
let answered = 0;
const total = replyWidgets().length;
replyWidgets().forEach(b => {
const name = b.dataset.item;
const checked = b.querySelector(`input[name="${name}"]:checked`);
const comment = b.querySelector('textarea').value.trim();
if (checked || comment) { answered++; b.classList.add('filled'); }
else { b.classList.remove('filled'); }
});
progressEl.textContent = `${answered} of ${total} items answered`;
}
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(readState()));
saveMarkEl.textContent = 'Saved ✓';
setTimeout(() => { saveMarkEl.textContent = 'Replies are saved in your browser as you type'; }, 1500);
refresh();
}
function load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) applyState(JSON.parse(raw));
} catch (e) {}
refresh();
}
function composeText() {
const now = new Date().toISOString().slice(0,10);
let text = `ePhyto Hub — BAFRA replies (v2)\nDate: ${now}\nService: Phytosanitary certificate (export of plants and plant products)\n\n`;
let answered = 0;
replyWidgets().forEach(b => {
const title = b.dataset.title;
const name = b.dataset.item;
const checked = b.querySelector(`input[name="${name}"]:checked`);
const comment = b.querySelector('textarea').value.trim();
const decision = checked ? (decisionLabels[checked.value] || checked.value) : '(no decision)';
if (checked || comment) answered++;
text += `--- ${title} ---\nDecision: ${decision}\n`;
if (comment) text += `Comment: ${comment}\n`;
text += '\n';
});
return { text, answered, total: replyWidgets().length };
}
function composeJson() {
const now = new Date().toISOString().slice(0,10);
const replies = {};
replyWidgets().forEach(b => {
const name = b.dataset.item;
const checked = b.querySelector(`input[name="${name}"]:checked`);
const comment = b.querySelector('textarea').value.trim();
replies[name] = {
title: b.dataset.title,
decision: checked ? checked.value : null,
decision_label: checked ? (decisionLabels[checked.value] || checked.value) : null,
comment
};
});
return { schema: 'bafra-ephyto-replies-v2', service: 'Phytosanitary certificate — Bhutan', date: now, replies };
}
function clearOne(widget) {
const name = widget.dataset.item;
widget.querySelectorAll(`input[name="${name}"]`).forEach(r => r.checked = false);
widget.querySelector('textarea').value = '';
save();
statusEl.textContent = `Cleared "${widget.dataset.title}".`;
}
function clearAll() {
if (!confirm('Clear all answers? This cannot be undone.')) return;
replyWidgets().forEach(b => {
const name = b.dataset.item;
b.querySelectorAll(`input[name="${name}"]`).forEach(r => r.checked = false);
b.querySelector('textarea').value = '';
});
save();
statusEl.textContent = 'All answers cleared.';
}
document.addEventListener('input', save);
document.addEventListener('change', save);
document.addEventListener('click', e => {
if (e.target.matches('.clearOne')) clearOne(e.target.closest('.reply'));
});
document.getElementById('downloadBtn').addEventListener('click', () => {
const data = composeJson();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bafra-ephyto-replies-${data.date}.json`;
a.click();
URL.revokeObjectURL(url);
statusEl.textContent = 'Downloaded — attach this file to your email or chat.';
});
document.getElementById('uploadInput').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
try {
const data = JSON.parse(ev.target.result);
if (data && data.replies) {
applyState(data.replies);
save();
statusEl.textContent = `Loaded replies from ${file.name}.`;
} else {
statusEl.textContent = 'File does not look like a replies JSON.';
}
} catch (err) {
statusEl.textContent = 'Could not read the file (not valid JSON).';
}
e.target.value = '';
};
reader.readAsText(file);
});
document.getElementById('copyBtn').addEventListener('click', async () => {
const { text, answered, total } = composeText();
try {
await navigator.clipboard.writeText(text);
statusEl.textContent = `Copied to clipboard (${answered}/${total} items answered).`;
} catch (e) {
statusEl.textContent = 'Could not copy — use "Preview the text" and copy manually.';
}
});
document.getElementById('mailBtn').addEventListener('click', e => {
e.preventDefault();
const { text } = composeText();
const subject = encodeURIComponent('ePhyto Hub — BAFRA replies');
const body = encodeURIComponent(text);
window.location.href = `mailto:?subject=${subject}&body=${body}`;
});
document.getElementById('previewBtn').addEventListener('click', () => {
const { text, answered, total } = composeText();
previewEl.textContent = text;
previewEl.classList.add('show');
statusEl.textContent = `${answered}/${total} items answered.`;
});
document.getElementById('clearAllBtn').addEventListener('click', clearAll);
load();
</script>
</body>
</html>