{
  "kind": "html",
  "title": "Your Saturday in Porto: keep or skip?",
  "html": "<style>\n*{box-sizing:border-box}\nbody{margin:0;background:#fbf6ef;color:#2b2420;font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;min-height:100vh}\n.wrap{max-width:520px;margin:0 auto;padding:20px 18px 28px}\n.eyebrow{font-size:13px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#a8432d}\nh1{font-size:24px;line-height:1.2;margin:4px 0 4px;letter-spacing:-.01em}\n.sub{color:#6b5d54;font-size:15px;margin:0 0 16px}\n.lbl{font-size:13px;font-weight:700;color:#6b5d54;margin:0 0 8px}\n.chips{display:flex;gap:8px;margin-bottom:14px}\n.chip{flex:1;font:inherit;font-weight:700;font-variant-numeric:tabular-nums;min-height:48px;border-radius:999px;border:1.5px solid #e4d6c6;background:#fff;color:#2b2420;cursor:pointer;transition:all .18s}\n.chip.on{background:#c8553d;border-color:#c8553d;color:#fff;box-shadow:0 4px 12px rgba(200,85,61,.28)}\n.sum{display:grid;grid-template-columns:repeat(3,1fr);background:#fff;border-radius:16px;box-shadow:0 2px 10px rgba(43,36,32,.07);margin-bottom:18px}\n.sum div{padding:12px 6px;text-align:center}\n.sum div+div{border-left:1px solid #f0e6da}\n.sum b{display:block;font-size:20px;font-variant-numeric:tabular-nums;color:#1f6f6f;transition:color .2s}\n.sum span{font-size:12px;color:#6b5d54}\n.stop{display:grid;grid-template-columns:46px 1fr;gap:8px;position:relative}\n.time{font-weight:700;font-size:15px;font-variant-numeric:tabular-nums;padding-top:14px;text-align:right;transition:opacity .2s;line-height:1.2}\n.time small{display:block;font-weight:500;font-size:12px;color:#6b5d54}\n.card{display:flex;align-items:center;gap:12px;background:#fff;border-radius:16px;padding:12px;box-shadow:0 2px 10px rgba(43,36,32,.07);transition:opacity .2s,transform .2s;border-left:4px solid #c8553d}\n.ico{flex:none;width:40px;height:40px;border-radius:12px;background:#f8e7e1;display:grid;place-content:center;font-size:21px}\n.txt{flex:1;min-width:0}\n.txt b{display:block;font-size:16px;line-height:1.25}\n.txt small{color:#6b5d54;font-size:13px}\n.tg{flex:none;font:inherit;font-size:14px;font-weight:700;min-width:68px;min-height:48px;border-radius:12px;border:1.5px solid #1f6f6f;background:#e5f1f0;color:#1f6f6f;cursor:pointer;transition:all .18s}\n.stop.off .card{background:#f6efe6;box-shadow:none;border-left-color:#d8ccc0}\n.stop.off .ico,.stop.off .txt{opacity:.45}\n.stop.off .time{opacity:.35;text-decoration:line-through}\n.stop.off .tg{background:#fff;border-color:#cdbfb2;color:#5a4d44}\n.walk{display:grid;grid-template-columns:46px 1fr;gap:8px;height:34px}\n.walk i{display:block;border-left:2px dashed #d9c7b6;margin-left:18px;height:100%}\n.walk span{font-size:13px;color:#1f6f6f;font-weight:600;align-self:center;font-variant-numeric:tabular-nums}\n.walk.off span{color:#a89a8e}\n.foot{margin-top:20px}\nbutton.main{width:100%;font:inherit;font-weight:700;font-size:17px;border:0;border-radius:16px;padding:16px;min-height:56px;background:#c8553d;color:#fff;cursor:pointer;box-shadow:0 6px 16px rgba(200,85,61,.3);transition:transform .15s,opacity .2s}\nbutton.alt{width:100%;font:inherit;font-weight:600;border:0;border-radius:14px;padding:14px;min-height:48px;background:transparent;color:#1f6f6f;cursor:pointer;margin-top:6px}\nbutton:active{transform:scale(.98)}\nbutton:disabled{opacity:.5;cursor:default;transform:none}\n.status{text-align:center;color:#6b5d54;font-size:14px;min-height:20px;margin-top:6px}\n.done{color:#1f6f6f}\n</style>\n<div class=\"wrap\">\n  <div class=\"eyebrow\" id=\"eyebrow\"></div>\n  <h1 id=\"title\"></h1>\n  <p class=\"sub\" id=\"sub\"></p>\n  <p class=\"lbl\" id=\"startLbl\"></p>\n  <div class=\"chips\" id=\"chips\"></div>\n  <div class=\"sum\"><div><b id=\"sEnd\"></b><span id=\"sEndL\"></span></div><div><b id=\"sWalk\"></b><span id=\"sWalkL\"></span></div><div><b id=\"sKm\"></b><span id=\"sKmL\"></span></div></div>\n  <div id=\"line\"></div>\n  <div class=\"foot\"><button class=\"main\" id=\"go\"></button><button class=\"alt\" id=\"alt\"></button><div class=\"status\" id=\"status\"></div></div>\n</div>\n<script>\n// The card's content. Change only this object.\n// answer.data: {start:\"09:30\", keep:[ids], skip:[ids], ends_at:\"18:40\", walk_minutes:57} or {action:\"replan\"}\nvar CARD = {\n  eyebrow: \"Porto · Sat 14 Nov\",\n  title: \"Your Saturday in Porto: keep or skip?\",\n  sub: \"I planned six stops. Skip any you don't fancy and the day re-times itself.\",\n  startLabel: \"Leave the hotel at\",\n  starts: [\"09:00\", \"09:30\", \"10:00\"],\n  start: \"09:30\",\n  stats: { end: \"back by\", walk: \"min walking\", km: \"km on foot\" },\n  walkWord: \"min walk\", keepWord: \"✓ Keep\", skipWord: \"Skipped\",\n  // x, y: rough map position in km, used to time the walk between kept stops\n  stops: [\n    { id: \"livraria\", icon: \"📚\", name: \"Livraria Lello\", note: \"Booked entry\", stay: 45, x: 0, y: 0, keep: true },\n    { id: \"clerigos\", icon: \"🗼\", name: \"Clérigos Tower\", note: \"Best city view\", stay: 40, x: 0.15, y: -0.1, keep: true },\n    { id: \"bolhao\", icon: \"🐟\", name: \"Mercado do Bolhão\", note: \"Lunch at the market\", stay: 90, x: 0.9, y: 0.5, keep: true },\n    { id: \"serralves\", icon: \"🌳\", name: \"Serralves Park\", note: \"Lovely, but far out west\", stay: 120, x: -4.0, y: 1.0, keep: false },\n    { id: \"ribeira\", icon: \"⛵\", name: \"Ribeira riverside\", note: \"Walk the Dom Luís bridge\", stay: 120, x: 0.4, y: -0.9, keep: true },\n    { id: \"gaia\", icon: \"🍷\", name: \"Graham's cellars\", note: \"Port tasting, 3 glasses\", stay: 90, x: -0.3, y: -1.6, keep: true }\n  ],\n  walkKmh: 4.8,\n  primary: \"Lock in {n} stops\",\n  primaryOne: \"Lock in 1 stop\",\n  emptyPrimary: \"Keep at least one stop\",\n  secondary: \"Plan a different day\",\n  sending: \"Sending…\",\n  doneTitle: \"Day locked in\",\n  doneSub: \"I'll put it in your calendar with walking directions.\",\n  preview: \"Preview only: open this card in Bridgit to answer.\"\n};\n\nvar sent = false, start = CARD.start;\nfunction $(id){ return document.getElementById(id); }\nfunction mins(t){ var p = t.split(\":\"); return +p[0] * 60 + +p[1]; }\nfunction hhmm(m){ var h = Math.floor(m / 60) % 24, x = m % 60; return (h < 10 ? \"0\" : \"\") + h + \":\" + (x < 10 ? \"0\" : \"\") + x; }\nfunction dur(m){ return m >= 60 && m % 60 === 0 ? m / 60 + \"\\u00a0h\" : m + \"\\u00a0min\"; }\nfunction el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }\n\n$(\"eyebrow\").textContent = CARD.eyebrow; $(\"title\").textContent = CARD.title; $(\"sub\").textContent = CARD.sub;\n$(\"startLbl\").textContent = CARD.startLabel; $(\"alt\").textContent = CARD.secondary;\n$(\"sEndL\").textContent = CARD.stats.end; $(\"sWalkL\").textContent = CARD.stats.walk; $(\"sKmL\").textContent = CARD.stats.km;\n\nCARD.starts.forEach(function(t){\n  var b = el(\"button\", \"chip\", t);\n  b.onclick = function(){ if (sent) return; start = t; render(); };\n  b.dataset.t = t; $(\"chips\").appendChild(b);\n});\n\nvar rows = CARD.stops.map(function(s, i){\n  var w = null;\n  if (i > 0) { w = el(\"div\", \"walk\"); w.appendChild(el(\"i\")); w.appendChild(el(\"span\")); $(\"line\").appendChild(w); }\n  var r = el(\"div\", \"stop\"), t = el(\"div\", \"time\"), c = el(\"div\", \"card\"), tx = el(\"div\", \"txt\"), tg = el(\"button\", \"tg\");\n  c.appendChild(el(\"div\", \"ico\", s.icon));\n  tx.appendChild(el(\"b\", null, s.name)); tx.appendChild(el(\"small\", null, s.note)); c.appendChild(tx);\n  tg.onclick = function(){ if (sent) return; s.keep = !s.keep; render(); };\n  c.appendChild(tg); r.appendChild(t); r.appendChild(c); $(\"line\").appendChild(r);\n  return { s: s, row: r, time: t, tg: tg, walk: w };\n});\n\nfunction render(){\n  document.querySelectorAll(\".chip\").forEach(function(b){ b.classList.toggle(\"on\", b.dataset.t === start); });\n  var now = mins(start), walk = 0, km = 0, prev = null, n = 0;\n  rows.forEach(function(r){\n    var s = r.s;\n    if (r.walk) r.walk.classList.toggle(\"off\", !s.keep);\n    if (s.keep) {\n      if (prev) {\n        var d = Math.hypot(s.x - prev.x, s.y - prev.y) * 1.3, m = Math.round(d / CARD.walkKmh * 60);\n        now += m; walk += m; km += d;\n        if (r.walk) r.walk.lastChild.textContent = \"🚶 \" + m + \" \" + CARD.walkWord;\n      } else if (r.walk) r.walk.lastChild.textContent = \"\";\n      r.time.textContent = hhmm(now); r.time.appendChild(el(\"small\", null, dur(s.stay))); now += s.stay; prev = s; n++;\n    } else {\n      if (r.walk) r.walk.lastChild.textContent = \"\";\n      r.time.textContent = \"\";\n    }\n    r.row.classList.toggle(\"off\", !s.keep);\n    r.tg.textContent = s.keep ? CARD.keepWord : CARD.skipWord;\n    r.tg.setAttribute(\"aria-pressed\", s.keep ? \"true\" : \"false\");\n  });\n  $(\"sEnd\").textContent = n ? hhmm(now) : \"–\";\n  $(\"sWalk\").textContent = walk; $(\"sKm\").textContent = km.toFixed(1);\n  var go = $(\"go\");\n  go.disabled = !n || sent;\n  go.textContent = !n ? CARD.emptyPrimary : n === 1 ? CARD.primaryOne : CARD.primary.replace(\"{n}\", n);\n  return { ends: hhmm(now), walk: walk };\n}\n\nfunction send(answer){\n  if (sent) return;\n  var B = window.Bridgit || window.Bridget;\n  if (!B || !B.respond) { $(\"status\").textContent = CARD.preview; return; }\n  sent = true;\n  document.querySelectorAll(\"button\").forEach(function(b){ b.disabled = true; });\n  $(\"status\").textContent = CARD.sending;\n  B.respond(JSON.stringify(answer));\n}\n$(\"go\").onclick = function(){\n  var r = render();\n  send({ start: start,\n    keep: CARD.stops.filter(function(s){ return s.keep; }).map(function(s){ return s.id; }),\n    skip: CARD.stops.filter(function(s){ return !s.keep; }).map(function(s){ return s.id; }),\n    ends_at: r.ends, walk_minutes: r.walk });\n};\n$(\"alt\").onclick = function(){ send({ action: \"replan\" }); };\ndocument.addEventListener(\"bridgit:answered\", function(e){\n  e.preventDefault();\n  $(\"title\").textContent = \"✓ \" + CARD.doneTitle; $(\"title\").className = \"done\";\n  $(\"sub\").textContent = CARD.doneSub; $(\"status\").textContent = \"\";\n  setTimeout(function(){ $(\"title\").scrollIntoView({ block: \"center\" }); }, 400);\n});\nrender();\n</script>\n",
  "expires_in_seconds": 43200
}
