{"name": "Wetter Dachau", "always_run_in_app": false, "icon": {"color": "orange", "glyph": "cloud-sun"}, "script": "// Variables used by Scriptable.\n// These must be at the very top of the file. Do not edit.\n// icon-color: orange; icon-glyph: cloud-sun;\n/* Wetter Dachau — Home-Screen-Widget im Stil der Seite „Klarer Himmel\"\n   Größen: klein (jetzt + Regen), mittel (+ 6-h-Vorschau), groß (+ 4 Tage mit Tagesabschnitten + Regen-Rückblick 12 h)\n   Icons: die farbigen Wettersymbole der Seite als PNG (wetter-dachau.pages.dev/widget/icons/), lokal gecacht.\n   Daten: Open-Meteo (Modell/Vorhersage) + Bright Sky (DWD-Messwerte für den Regen-Rückblick)\n   Tippen öffnet https://wetter-dachau.pages.dev */\n\nconst LOC = { name: \"Dachau\", lat: 48.26, lon: 11.43 };\nconst PAGE_URL = \"https://wetter-dachau.pages.dev\";\nconst ICON_BASE = PAGE_URL + \"/widget/icons/\";\nconst REFRESH_MIN = 30;\n\n/* ---- Palette (hell / dunkel) wie auf der Seite ---- */\nconst C = {\n  bg:      Color.dynamic(new Color(\"#F7F5F1\"), new Color(\"#1E2126\")),\n  ink:     Color.dynamic(new Color(\"#23282F\"), new Color(\"#EDEEF0\")),\n  ink2:    Color.dynamic(new Color(\"#5C6470\"), new Color(\"#A6ADB8\")),\n  ink3:    Color.dynamic(new Color(\"#979FAA\"), new Color(\"#6D7480\")),\n  line:    Color.dynamic(new Color(\"#E9E5DC\"), new Color(\"#2C3038\")),\n  accent:  Color.dynamic(new Color(\"#C07C00\"), new Color(\"#FFC24A\")),\n  rain:    Color.dynamic(new Color(\"#2273CE\"), new Color(\"#5CA5EE\")),\n  rainSoft:Color.dynamic(new Color(\"#C9DFF5\"), new Color(\"#2A4058\")),\n  tmax:    Color.dynamic(new Color(\"#E2503C\"), new Color(\"#F0715E\")),\n  tmin:    Color.dynamic(new Color(\"#3D7BC4\"), new Color(\"#6FA4E0\")),\n  sun:     new Color(\"#F6B31B\"),\n  wind:    Color.dynamic(new Color(\"#0E9B8F\"), new Color(\"#3DBDB0\")),\n};\nconst WMO_TEXT = {0:\"Sonnig\",1:\"Überwiegend sonnig\",2:\"Wolkig\",3:\"Bedeckt\",45:\"Nebel\",48:\"Reifnebel\",\n  51:\"Niesel\",53:\"Niesel\",55:\"Starker Niesel\",56:\"Gefr. Niesel\",57:\"Gefr. Niesel\",61:\"Leichter Regen\",63:\"Regen\",\n  65:\"Starker Regen\",66:\"Gefr. Regen\",67:\"Gefr. Regen\",71:\"Leichter Schneefall\",73:\"Schneefall\",75:\"Starker Schneefall\",\n  77:\"Schneegriesel\",80:\"Leichte Schauer\",81:\"Schauer\",82:\"Starke Schauer\",85:\"Schneeschauer\",86:\"Schneeschauer\",\n  95:\"Gewitter\",96:\"Gewitter mit Hagel\",99:\"Gewitter mit Hagel\"};\nconst WD = [\"So\", \"Mo\", \"Di\", \"Mi\", \"Do\", \"Fr\", \"Sa\"];\nconst pad2 = n => String(n).padStart(2, \"0\");\nconst fmtMm = v => v == null ? \"–\" : v < .05 ? \"0\" : (Math.round(v * 10) / 10).toFixed(1).replace(\".\", \",\");\nconst round = v => v == null ? \"–\" : Math.round(v);\n\n/* ---- Symbol-Spezifikation (1:1 wie auf der Seite) ---- */\nfunction specFromCode(code, night, cc) {\n  const sp = { night: !!night, fog: false, thunder: false, rain: 0, snow: 0,\n    cc: cc != null ? cc : (code === 0 ? 5 : code === 1 ? 35 : code === 2 ? 65 : 95) };\n  if (code === 45 || code === 48) sp.fog = true;\n  else if (code >= 95) sp.thunder = true;\n  else if ([51,53,55,56,57].includes(code)) sp.rain = 1;\n  else if ([61,66,80].includes(code)) sp.rain = 2;\n  else if ([63,81].includes(code)) sp.rain = 2;\n  else if ([65,67,82].includes(code)) sp.rain = 3;\n  else if ([71,77,85].includes(code)) sp.snow = 1;\n  else if ([73,75,86].includes(code)) sp.snow = 2;\n  return sp;\n}\n/* Spezifikation → Dateiname des vorgerenderten Icons (gleiche Verzweigung wie iconSVG auf der Seite) */\nfunction iconKey(sp) {\n  if (sp.fog) return \"fog\";\n  const wet = sp.rain || sp.snow || sp.thunder;\n  let cc = sp.cc; if (wet) cc = Math.max(cc, 55);\n  const bucket = cc < 15 ? 5 : cc < 45 ? 35 : cc < 82 ? 65 : 95;\n  const dn = sp.night ? \"n\" : \"d\";\n  const type = sp.thunder ? \"thunder\" : sp.rain ? \"rain\" + Math.min(3, sp.rain) : sp.snow ? \"snow\" + Math.min(2, sp.snow) : \"clear\";\n  return `${type}_${dn}_${bucket}`;\n}\n\n/* ---- Daten ---- */\nconst fm = FileManager.local();\nconst CACHE = fm.joinPath(fm.cacheDirectory(), \"wetter-dachau-widget.json\");\nconst ICON_DIR = fm.joinPath(fm.cacheDirectory(), \"wetter-dachau-icons\");\n\nasync function fetchJSON(url, timeout) {\n  const r = new Request(url);\n  r.timeoutInterval = timeout || 12;\n  return await r.loadJSON();\n}\nfunction localNowIso(offsetSec) {\n  return new Date(Date.now() + (offsetSec || 0) * 1000).toISOString().slice(0, 16);\n}\nasync function loadData() {\n  const fcUrl = `https://api.open-meteo.com/v1/forecast?latitude=${LOC.lat}&longitude=${LOC.lon}`\n    + \"&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,wind_gusts_10m,is_day,precipitation\"\n    + \"&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,is_day,cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,sunshine_duration,wind_speed_10m,wind_direction_10m,wind_gusts_10m\"\n    + \"&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,sunshine_duration\"\n    + \"&past_days=1&forecast_days=5&timezone=auto\";\n  const fc = await fetchJSON(fcUrl);\n  const nowIso = localNowIso(fc.utc_offset_seconds);\n  /* Regen-Rückblick: Messwerte der nächsten DWD-Station (Bright Sky), nur echte Beobachtungen */\n  let meas = null;\n  try {\n    const from = new Date(Date.now() - 26 * 3600e3 + fc.utc_offset_seconds * 1000).toISOString().slice(0, 16);\n    const bs = await fetchJSON(`https://api.brightsky.dev/weather?lat=${LOC.lat}&lon=${LOC.lon}`\n      + `&date=${from}&last_date=${nowIso}&tz=${encodeURIComponent(fc.timezone || \"Europe/Berlin\")}`, 8);\n    const src = {}; for (const s of bs.sources || []) src[s.id] = s;\n    const byHour = {}; let station = null;\n    for (const r of bs.weather || []) {\n      const s = src[r.source_id];\n      if (!s || s.observation_type === \"forecast\" || r.precipitation == null) continue;\n      byHour[r.timestamp.slice(0, 13)] = r.precipitation;\n      station = station || s.station_name;\n    }\n    if (station) meas = { byHour, station: station.replace(/\\b\\w+/g, w => w.length > 3 ? w[0] + w.slice(1).toLowerCase() : w) };\n  } catch (e) {}\n  const data = { fc, meas, nowIso, fetched: Date.now() };\n  try { fm.writeString(CACHE, JSON.stringify(data)); } catch (e) {}\n  return data;\n}\nfunction loadCached() {\n  try { if (fm.fileExists(CACHE)) return JSON.parse(fm.readString(CACHE)); } catch (e) {}\n  return null;\n}\n/* Icons: einmal laden, danach aus dem lokalen Cache */\nconst iconMem = {};\nasync function loadIcon(key) {\n  if (iconMem[key]) return iconMem[key];\n  try { if (!fm.fileExists(ICON_DIR)) fm.createDirectory(ICON_DIR, true); } catch (e) {}\n  const path = fm.joinPath(ICON_DIR, key + \".png\");\n  let img = null;\n  try { if (fm.fileExists(path)) img = fm.readImage(path); } catch (e) {}\n  if (!img) {\n    try {\n      const r = new Request(ICON_BASE + key + \".png\"); r.timeoutInterval = 10;\n      img = await r.loadImage();\n      fm.writeImage(path, img);\n    } catch (e) { img = null; }\n  }\n  iconMem[key] = img;\n  return img;\n}\nasync function loadIcons(keys) {\n  await Promise.all([...new Set(keys)].map(k => loadIcon(k)));\n}\n\n/* ---- Auswertung (Tagesabschnitte und Tagessymbol wie auf der Seite) ---- */\nfunction effCC(h, k) {\n  if (h.cloud_cover_low) return Math.max(h.cloud_cover_low[k], h.cloud_cover_mid[k] * .9, h.cloud_cover_high[k] * .35);\n  return h.cloud_cover[k];\n}\nfunction dayParts(h, base) {\n  const parts = [], defs = [[\"Vorm.\", 6, 12], [\"Nachm.\", 12, 18], [\"Abend\", 18, 24], [\"Nacht\", 24, 30]];\n  for (const [label, a, b] of defs) {\n    const idx = [];\n    for (let k = base + a; k < base + b && k < h.time.length; k++) idx.push(k);\n    if (!idx.length) { parts.push(null); continue; }\n    let wsum = 0, gmax = 0, psum = 0, pmax = 0, ccsum = 0, sunsum = 0;\n    const codes = idx.map(k => h.weather_code[k]);\n    for (const k of idx) {\n      wsum += h.wind_speed_10m[k]; gmax = Math.max(gmax, h.wind_gusts_10m[k]);\n      psum += h.precipitation[k]; pmax = Math.max(pmax, h.precipitation_probability[k] || 0);\n      ccsum += effCC(h, k); sunsum += h.sunshine_duration[k] || 0;\n    }\n    const night = label === \"Nacht\";\n    const sunPct = sunsum / (idx.length * 36);\n    const spec = { night, cc: night ? ccsum / idx.length : Math.min(ccsum / idx.length, 100 - sunPct), fog: false, thunder: false, rain: 0, snow: 0 };\n    const snowy = codes.some(c => [71,73,75,77,85,86].includes(c));\n    if (codes.some(c => c >= 95)) spec.thunder = true;\n    else if (psum >= .2 && snowy) spec.snow = psum > 2 ? 2 : 1;\n    else if (psum >= .2 || codes.some(c => c >= 51 && c <= 82 && psum >= .1)) spec.rain = psum > 5 ? 3 : psum > 1.5 ? 2 : 1;\n    else if (codes.filter(c => c === 45 || c === 48).length > idx.length / 2) spec.fog = true;\n    parts.push({ label, spec, wind: wsum / idx.length, gust: gmax, precip: psum, prob: pmax });\n  }\n  return parts;\n}\nfunction daySpec(h, base) {\n  const idx = [];\n  for (let k = base + 8; k < base + 18 && k < h.time.length; k++) idx.push(k);\n  let cc = 0, psum = 0, sunsum = 0;\n  const codes = idx.map(k => h.weather_code[k]);\n  for (const k of idx) { cc += effCC(h, k); psum += h.precipitation[k]; sunsum += h.sunshine_duration[k] || 0; }\n  const spec = { night: false, cc: Math.min(cc / idx.length, 100 - sunsum / (idx.length * 36)), fog: false, thunder: false, rain: 0, snow: 0 };\n  const snowy = codes.some(c => [71,73,75,77,85,86].includes(c));\n  if (codes.some(c => c >= 95)) spec.thunder = true;\n  else if (psum >= .3 && snowy) spec.snow = psum > 2 ? 2 : 1;\n  else if (psum >= .3) spec.rain = psum > 5 ? 3 : psum > 1.5 ? 2 : 1;\n  else if (codes.filter(c => c === 45 || c === 48).length > idx.length / 2) spec.fog = true;\n  return spec;\n}\nfunction analyse(d) {\n  const { fc, meas, nowIso } = d, h = fc.hourly, c = fc.current, dd = fc.daily;\n  const hourKey = nowIso.slice(0, 13);\n  let hi = h.time.findIndex(t => t.slice(0, 13) >= hourKey); if (hi < 0) hi = h.time.length - 1;\n  const todayIdx = dd.time.findIndex(t => t === nowIso.slice(0, 10)); const ti = todayIdx < 0 ? 1 : todayIdx;\n  const nowSpec = specFromCode(c.weather_code, !c.is_day, effCC(h, hi));\n  /* letzte Stunden: Balken „08\" = 07–08 Uhr, also Stunden mit Zeitstempel <= jetzt */\n  const past = [];\n  for (let k = hi; k >= 0 && past.length < 24; k--) {\n    const key = h.time[k].slice(0, 13);\n    past.unshift({ t: h.time[k], model: h.precipitation[k] || 0, meas: meas ? (meas.byHour[key] ?? null) : null });\n  }\n  const sum = (arr, key, n) => {\n    const v = arr.slice(-n).map(x => x[key]).filter(x => x != null);\n    return v.length ? v.reduce((a, b) => a + b, 0) : null;\n  };\n  const rain = { m3: sum(past, \"meas\", 3), m24: sum(past, \"meas\", 24), o3: sum(past, \"model\", 3), o24: sum(past, \"model\", 24), station: meas ? meas.station : null };\n  /* Regnet es gerade? Dann: bis wann. Sonst: nächster Regen in 18 h */\n  const rainingNow = (c.precipitation || 0) > 0 || (c.weather_code >= 51 && c.weather_code <= 67) || (c.weather_code >= 80 && c.weather_code <= 99);\n  let nextRain = null, dryAt = null, dryFound = false;\n  if (rainingNow) {\n    for (let k = hi + 1; k < Math.min(hi + 13, h.time.length); k++)\n      if ((h.precipitation_probability[k] || 0) < 40 && (h.precipitation[k] || 0) < .2) { dryAt = h.time[k].slice(11, 13); dryFound = true; break; }\n  } else {\n    for (let k = hi + 1; k < Math.min(hi + 19, h.time.length); k++)\n      if ((h.precipitation_probability[k] || 0) >= 40 || (h.precipitation[k] || 0) >= .2) { nextRain = h.time[k].slice(11, 13); break; }\n  }\n  const next6 = [];\n  for (let k = hi + 1; k <= hi + 6 && k < h.time.length; k++)\n    next6.push({ hh: h.time[k].slice(11, 13), spec: specFromCode(h.weather_code[k], !h.is_day[k], effCC(h, k)), temp: h.temperature_2m[k], pp: h.precipitation_probability[k] || 0 });\n  const days = [];\n  for (let i = ti; i < Math.min(ti + 4, dd.time.length); i++) {\n    const dt = new Date(dd.time[i] + \"T12:00\");\n    const base = h.time.findIndex(t => t.slice(0, 10) === dd.time[i]);\n    if (base < 0) continue;\n    days.push({ name: i === ti ? \"heute\" : i === ti + 1 ? \"morgen\" : [\"Sonntag\",\"Montag\",\"Dienstag\",\"Mittwoch\",\"Donnerstag\",\"Freitag\",\"Samstag\"][dt.getDay()],\n      date: `${i < ti + 2 ? WD[dt.getDay()] + \"., \" : \"\"}${pad2(dt.getDate())}.${pad2(dt.getMonth() + 1)}.`,\n      spec: daySpec(h, base), parts: dayParts(h, base),\n      max: dd.temperature_2m_max[i], min: dd.temperature_2m_min[i], pp: dd.precipitation_probability_max[i] || 0,\n      sunH: Math.round((dd.sunshine_duration[i] || 0) / 3600) });\n  }\n  return { c, nowSpec, rainingNow, dryAt, dryFound, today: { max: dd.temperature_2m_max[ti], min: dd.temperature_2m_min[ti] }, rain, past, nextRain, next6, days };\n}\nfunction neededIcons(a, family) {\n  const keys = [iconKey(a.nowSpec)];\n  if (family !== \"small\") for (const n of a.next6) keys.push(iconKey(n.spec));\n  if (family === \"large\") for (const d of a.days) { keys.push(iconKey(d.spec)); for (const p of d.parts) if (p) keys.push(iconKey(p.spec)); }\n  return keys;\n}\n\n/* ---- Widget-Bausteine ---- */\nfunction text(stack, str, size, color, weight) {\n  const t = stack.addText(String(str));\n  t.font = weight === \"bold\" ? Font.boldSystemFont(size) : weight === \"semi\" ? Font.semiboldSystemFont(size)\n    : weight === \"medium\" ? Font.mediumSystemFont(size) : Font.systemFont(size);\n  t.textColor = color || C.ink;\n  t.lineLimit = 1;\n  t.minimumScaleFactor = .7;\n  return t;\n}\nfunction sym(stack, name, size, color) {\n  const s = SFSymbol.named(name); s.applyFont(Font.systemFont(size));\n  const img = stack.addImage(s.image); img.imageSize = new Size(size, size); img.tintColor = color || C.ink; img.resizable = true;\n  return img;\n}\n/* farbiges Wettersymbol (54:44) — Fallback auf SF Symbol, falls das PNG fehlt */\nfunction wicon(stack, spec, w) {\n  const img = iconMem[iconKey(spec)];\n  const h = Math.round(w * 44 / 54);\n  if (img) { const i = stack.addImage(img); i.imageSize = new Size(w, h); i.resizable = true; i.centerAlignImage(); return i; }\n  const name = spec.thunder ? \"cloud.bolt.rain.fill\" : spec.rain ? \"cloud.rain.fill\" : spec.snow ? \"cloud.snow.fill\" : spec.fog ? \"cloud.fog.fill\"\n    : spec.cc < 45 ? (spec.night ? \"moon.stars.fill\" : \"sun.max.fill\") : \"cloud.fill\";\n  return sym(stack, name, h, spec.cc < 45 && !spec.night && !spec.rain ? C.sun : C.ink2);\n}\nfunction centered(col, fn) { const r = col.addStack(); r.centerAlignContent(); r.addSpacer(); fn(r); r.addSpacer(); return r; }\n\nfunction rainLine(a) {\n  const r = a.rain, has = r.m24 != null;\n  return { v3: has ? r.m3 : r.o3, v24: has ? r.m24 : r.o24, src: has ? `gemessen ${r.station}` : \"Modell\" };\n}\nfunction buildHead(w, a, compact) {\n  const top = w.addStack(); top.centerAlignContent();\n  wicon(top, a.nowSpec, compact ? 54 : 64);\n  top.addSpacer(8);\n  const col = top.addStack(); col.layoutVertically();\n  const tl = col.addStack(); tl.bottomAlignContent();\n  text(tl, `${round(a.c.temperature_2m)}°`, compact ? 30 : 34, C.ink, \"semi\");\n  tl.addSpacer(6);\n  const hl = tl.addStack(); hl.layoutVertically(); hl.spacing = 0;\n  text(hl, `↑ ${round(a.today.max)}°`, 11, C.tmax, \"bold\");\n  text(hl, `↓ ${round(a.today.min)}°`, 11, C.tmin, \"bold\");\n  text(col, WMO_TEXT[a.c.weather_code] || \"\", 12, C.ink2, \"semi\");\n  top.addSpacer();\n}\nfunction addRainBlock(w, a) {\n  const rl = rainLine(a);\n  const row = w.addStack(); row.centerAlignContent();\n  sym(row, \"drop.fill\", 11, C.rain); row.addSpacer(4);\n  text(row, `${fmtMm(rl.v3)} l/m² · 3 h   ${fmtMm(rl.v24)} l/m² · 24 h`, 11, C.rain, \"semi\");\n  text(w, \"Regen \" + rl.src, 9.5, C.ink3);\n}\nfunction addInsight(w, a) {\n  const t = a.rainingNow ? (a.dryFound ? `Regen, trocken ab ca. ${a.dryAt} Uhr` : \"Regen hält die nächsten Stunden an\")\n    : a.nextRain ? `Ab ca. ${a.nextRain} Uhr Regen möglich` : \"Kein Regen in den nächsten 18 h\";\n  const row = w.addStack(); row.centerAlignContent();\n  sym(row, a.rainingNow || a.nextRain ? \"umbrella.fill\" : \"checkmark.circle\", 10, C.accent); row.addSpacer(4);\n  text(row, t, 10.5, C.accent, \"semi\");\n}\nfunction addNext6(w, a, iconW) {\n  const row = w.addStack(); row.spacing = 0;\n  a.next6.forEach((n, i) => {\n    const col = row.addStack(); col.layoutVertically();\n    centered(col, r => text(r, n.hh, 10, C.ink3, \"semi\"));\n    centered(col, r => wicon(r, n.spec, iconW || 30));\n    centered(col, r => text(r, `${round(n.temp)}°`, 12, C.ink, \"semi\"));\n    centered(col, r => text(r, n.pp > 0 ? `${n.pp} %` : \" \", 9, n.pp > 0 ? C.rain : C.ink3, \"semi\"));\n    if (i < a.next6.length - 1) row.addSpacer();\n  });\n}\n/* 4 Tage als Spalten wie auf der Seite: Datum, Tagname, Max/Min, Tagessymbol, vier Tagesabschnitte, Sonne, Regen */\nfunction addDayCols(w, a) {\n  const row = w.addStack(); row.topAlignContent();\n  a.days.forEach((d, i) => {\n    const col = row.addStack(); col.layoutVertically();\n    centered(col, r => text(r, d.date, 9, C.ink3, \"semi\"));\n    centered(col, r => text(r, d.name, 11.5, C.ink, \"bold\"));\n    col.addSpacer(2);\n    centered(col, r => wicon(r, d.spec, 40));\n    centered(col, r => { text(r, `${round(d.max)}°`, 12, C.tmax, \"bold\"); r.addSpacer(5); text(r, `${round(d.min)}°`, 12, C.tmin, \"bold\"); });\n    col.addSpacer(3);\n    const pr = col.addStack(); pr.centerAlignContent();\n    d.parts.forEach((p, j) => {\n      const pc = pr.addStack(); pc.layoutVertically();\n      centered(pc, r => { if (p) wicon(r, p.spec, 18); else text(r, \"–\", 9, C.ink3); });\n      centered(pc, r => text(r, p ? [\"Vm\", \"Nm\", \"Ab\", \"Na\"][j] : \" \", 7.5, C.ink3, \"semi\"));\n      if (j < 3) pr.addSpacer(1);\n    });\n    col.addSpacer(3);\n    centered(col, r => { sym(r, \"sun.max.fill\", 9, C.sun); r.addSpacer(3); text(r, `${d.sunH} h`, 9.5, C.ink2, \"semi\"); });\n    centered(col, r => { sym(r, \"drop.fill\", 8, C.rain); r.addSpacer(3); text(r, `${d.pp} %`, 9.5, C.rain, \"semi\"); });\n    if (i < a.days.length - 1) row.addSpacer(6);\n  });\n}\nfunction addRainBars(w, a) {\n  const hrs = a.past.slice(-12);\n  const W = 300, H = 40, colW = W / hrs.length;\n  const vmax = Math.max(1, ...hrs.map(x => Math.max(x.model, x.meas || 0)));\n  const ctx = new DrawContext(); ctx.size = new Size(W, H); ctx.opaque = false; ctx.respectScreenScale = true;\n  const yOf = v => 3 + (H - 15) * (1 - v / vmax);\n  ctx.setFillColor(C.line); ctx.fillRect(new Rect(0, H - 12, W, 1));\n  hrs.forEach((x, i) => {\n    const x0 = i * colW + 1.5, bw = colW - 3;\n    if (x.model > 0) { ctx.setFillColor(C.rainSoft); ctx.fillRect(new Rect(x0, yOf(x.model), bw, H - 12 - yOf(x.model))); }\n    if (x.meas != null && x.meas > 0) { ctx.setFillColor(C.rain); ctx.fillRect(new Rect(x0 + bw * .25, yOf(x.meas), bw * .5, H - 12 - yOf(x.meas))); }\n    if ((hrs.length - 1 - i) % 3 === 0) {\n      ctx.setTextAlignedCenter(); ctx.setFont(Font.semiboldSystemFont(8)); ctx.setTextColor(C.ink3);\n      ctx.drawTextInRect(x.t.slice(11, 13), new Rect(i * colW, H - 11, colW, 11));\n    }\n  });\n  const img = w.addImage(ctx.getImage()); img.resizable = true;\n  text(w, a.rain.station ? `Regen-Rückblick 12 h · gemessen ${a.rain.station} (kräftig) vs. Modell (hell)` : \"Regen-Rückblick 12 h · Modell (keine Messwerte)\", 8.5, C.ink3);\n}\nfunction footer(w, d) {\n  const dt = new Date(d.fetched);\n  text(w, `${LOC.name} · Stand ${pad2(dt.getHours())}:${pad2(dt.getMinutes())}${d.stale ? \" (offline)\" : \"\"}`, 8.5, C.ink3);\n}\nfunction buildWidget(d, a, family) {\n  const w = new ListWidget();\n  w.backgroundColor = C.bg;\n  w.url = PAGE_URL;\n  w.setPadding(12, 14, 10, 14);\n  w.refreshAfterDate = new Date(Date.now() + REFRESH_MIN * 60e3);\n  if (family === \"small\") {\n    buildHead(w, a, true); w.addSpacer(6);\n    addRainBlock(w, a); w.addSpacer(4);\n    addInsight(w, a); w.addSpacer();\n    footer(w, d);\n  } else if (family === \"medium\") {\n    const row = w.addStack(); row.topAlignContent();\n    const left = row.addStack(); left.layoutVertically(); left.size = new Size(150, 0);\n    buildHead(left, a, true); left.addSpacer(6); addRainBlock(left, a); left.addSpacer(4); addInsight(left, a);\n    row.addSpacer(10);\n    const right = row.addStack(); right.layoutVertically();\n    right.addSpacer(2); text(right, \"Nächste 6 Stunden\", 10, C.ink3, \"semi\"); right.addSpacer(4);\n    addNext6(right, a, 28);\n    w.addSpacer(); footer(w, d);\n  } else {\n    buildHead(w, a, true);\n    w.addSpacer(4);\n    const r = w.addStack(); r.centerAlignContent();\n    const l = r.addStack(); l.layoutVertically(); addRainBlock(l, a); r.addSpacer();\n    const ri = r.addStack(); ri.layoutVertically(); addInsight(ri, a);\n    w.addSpacer(8);\n    addDayCols(w, a);\n    w.addSpacer(8);\n    addRainBars(w, a);\n    w.addSpacer();\n    footer(w, d);\n  }\n  return w;\n}\nfunction errorWidget(msg) {\n  const w = new ListWidget(); w.backgroundColor = C.bg; w.url = PAGE_URL;\n  text(w, \"Wetter Dachau\", 13, C.ink, \"semi\"); w.addSpacer(4);\n  const t = w.addText(msg); t.font = Font.systemFont(11); t.textColor = C.ink2;\n  w.refreshAfterDate = new Date(Date.now() + 10 * 60e3);\n  return w;\n}\n\n/* ---- Main ---- */\nconst family = config.widgetFamily || args.widgetParameter || \"medium\";\nlet data = null, widget;\ntry { data = await loadData(); }\ncatch (e) { data = loadCached(); if (data) data.stale = true; }\nif (!data) widget = errorWidget(\"Keine Daten (offline?) — antippen öffnet die Seite.\");\nelse {\n  const a = analyse(data);\n  await loadIcons(neededIcons(a, family));\n  widget = buildWidget(data, a, family);\n}\nif (config.runsInWidget) Script.setWidget(widget);\nelse if (family === \"small\") await widget.presentSmall();\nelse if (family === \"large\") await widget.presentLarge();\nelse await widget.presentMedium();\nScript.complete();\n", "share_sheet_inputs": []}