{
  "sha": "7ab6038e6fb68cefbf3d4c0703c568a4d61dab12",
  "message": "改善手機 LIVE 快訊跑馬燈速度與字型 (#65)\n\n依文字寬度控制手機跑馬燈為約 24 px/s，改用繁中系統字型並支援減少動態效果。",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "committer": {
    "login": "web-flow",
    "name": "",
    "email": ""
  },
  "created_at": "2026-08-09T22:07:18Z",
  "files": [
    {
      "filename": "static/app.js",
      "previous_filename": "",
      "status": "modified",
      "additions": 18,
      "deletions": 0,
      "changes": 18
    },
    {
      "filename": "static/site.css",
      "previous_filename": "",
      "status": "modified",
      "additions": 21,
      "deletions": 1,
      "changes": 22
    },
    {
      "filename": "templates/base.html",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "tests/test_mobile_layout.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 7,
      "deletions": 0,
      "changes": 7
    }
  ],
  "diff": "@@ -77,6 +77,24 @@\\n     });\\n   }\\n \\n+  /* — LIVE ticker: keep a readable speed even as headline length changes — */\\n+  var marqueeTrack = document.querySelector(\".marquee-track\");\\n+  function setMarqueeDuration() {\\n+    if (!marqueeTrack) return;\\n+    var tickerPass = marqueeTrack.querySelector(\".ticker-pass\");\\n+    if (!tickerPass) return;\\n+    var pixelsPerSecond = window.matchMedia(\"(max-width: 720px)\").matches ? 24 : 52;\\n+    var seconds = Math.max(20, tickerPass.scrollWidth / pixelsPerSecond);\\n+    marqueeTrack.style.setProperty(\"--ticker-duration\", seconds.toFixed(1) + \"s\");\\n+  }\\n+  if (marqueeTrack) {\\n+    setMarqueeDuration();\\n+    if (document.fonts && document.fonts.ready) {\\n+      document.fonts.ready.then(setMarqueeDuration);\\n+    }\\n+    window.addEventListener(\"resize\", setMarqueeDuration);\\n+  }\\n+\\n   /* — home: category filter over feed rows ([data-cat]) — */\\n   var catBtns = Array.prototype.slice.call(document.querySelectorAll(\"[data-filter-cat]\"));\\n   catBtns.forEach(function (b) {\\n@@ -35,6 +35,7 @@ a:hover { color: var(--color-accent-600); }\\n      ↗ mark + hover color (owner: clickable-looking but NO underline);\\n      the track pauses on hover so items can actually be clicked. — */\\n .marquee-track:hover { animation-play-state: paused; }\\n+.ticker-pass { padding-right: 70px; font-size: 13px; white-space: nowrap; }\\n .ticker-item { color: inherit; text-decoration: none; }\\n a.ticker-item:hover { color: var(--color-accent); }\\n a.ticker-item:hover .ticker-time { color: var(--color-accent); }\\n@@ -56,7 +57,10 @@ a.ticker-item:hover .ticker-time { color: var(--color-accent); }\\n   box-shadow: 0 0 32px rgba(0, 0, 0, .08);\\n }\\n .pulse-dot { width: 8px; height: 8px; background: var(--color-accent); animation: avwire-pulse 2s infinite; }\\n-.marquee-track { display: flex; width: max-content; animation: avwire-marquee 80s linear infinite; white-space: nowrap; }\\n+.marquee-track {\\n+  display: flex; width: max-content; white-space: nowrap;\\n+  animation: avwire-marquee var(--ticker-duration, 80s) linear infinite;\\n+}\\n \\n /* — Google-style search, deliberately the first row of every page — */\\n .header-search-row {\\n@@ -492,6 +496,22 @@ a.flash-row:hover { color: inherit; }\\n   }\\n   .nav-link.active { border-color: var(--color-accent); }\\n   .ticker-label { padding: 7px 10px; font-size: 11px; letter-spacing: .08em; }\\n+  .marquee-track { --ticker-duration: 180s; }\\n+  .ticker-pass {\\n+    font-family: -apple-system, BlinkMacSystemFont, \"PingFang TC\", \"Noto Sans TC\", sans-serif;\\n+    font-size: 14px; font-weight: 500; line-height: 1.5; letter-spacing: .01em;\\n+    -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;\\n+  }\\n+  .ticker-time {\\n+    font-family: \"SFMono-Regular\", ui-monospace, \"Roboto Mono\", Consolas, monospace;\\n+    font-size: 12px; font-weight: 700; letter-spacing: 0; font-variant-numeric: tabular-nums;\\n+  }\\n+}\\n+\\n+@media (prefers-reduced-motion: reduce) {\\n+  .ticker-window { overflow-x: auto; }\\n+  .marquee-track { animation: none; transform: none; }\\n+  .marquee-track .ticker-pass:nth-child(2) { display: none; }\\n }\\n \\n @media (max-width: 640px) {\\n@@ -88,7 +88,7 @@\\n     <div class=\"ticker-window\">\\n       <div class=\"marquee-track\">\\n         {% for pass in range(2) %}\\n-        <span style=\"padding-right:70px;font-size:13px;white-space:nowrap;\">\\n+        <span class=\"ticker-pass\">\\n           {%- for it in ticker_items -%}\\n           {%- if it.url -%}\\n           <a class=\"ticker-item\" href=\"{{ it.url }}\"{% if it.external %} target=\"_blank\" rel=\"noopener\"{% endif %}><b class=\"ticker-time{% if it.hot %} hot{% endif %}\">{{ it.time }}</b>&#8195;{{ it.text }}<span class=\"ticker-go\">↗</span></a>\\n@@ -25,6 +25,7 @@ def main() -> None:\\n     assert 'class=\"site-nav\" id=\"site-nav\"' in home\\n     assert 'class=\"utility-row\"' in home\\n     assert 'class=\"ticker-window\"' in home\\n+    assert 'class=\"ticker-pass\"' in home\\n     assert 'class=\"feed-copy\"' in home\\n     assert 'class=\"article-page\"' in article\\n     assert 'class=\"article-source-row\"' in article\\n@@ -34,9 +35,15 @@ def main() -> None:\\n     assert \"grid-template-columns: repeat(2, minmax(0, 1fr))\" in css\\n     assert \".center-col { order: 1;\" in css\\n     assert \".feed-copy { grid-column: 1 / -1; }\" in css\\n+    assert \"--ticker-duration: 180s\" in css\\n+    assert '\"PingFang TC\"' in css\\n+    assert \"@media (prefers-reduced-motion: reduce)\" in css\\n+    assert \".marquee-track .ticker-pass:nth-child(2) { display: none; }\" in css\\n     assert 'setAttribute(\"aria-expanded\"' in script\\n     assert 'event.key === \"Escape\"' in script\\n     assert 'matchMedia(\"(min-width: 721px)\")' in script\\n+    assert 'matches ? 24 : 52' in script\\n+    assert 'style.setProperty(\"--ticker-duration\"' in script\\n \\n     print(\"test_mobile_layout: OK\")\\n "
}
