{
  "version": "1.0",
  "updatedAt": "2026-07-16",
  "levels": [
    {
      "id": "imitate",
      "label": "模仿：先做得出來",
      "purpose": "照著明確範例完成基本動作，但每章都要留下自己的內容選擇。",
      "chapterRange": [1, 6]
    },
    {
      "id": "modify",
      "label": "改造：開始做選擇",
      "purpose": "保留技術骨架，主動改變資料、版型或互動規則，說明取捨。",
      "chapterRange": [7, 13]
    },
    {
      "id": "transfer",
      "label": "遷移：換題目也會做",
      "purpose": "把學過的解法搬到不同情境，先拆需求再決定資料與介面。",
      "chapterRange": [14, 20]
    },
    {
      "id": "create",
      "label": "創作：獨立定義作品",
      "purpose": "自己定義問題、受眾、功能與驗收證據，完成可公開說明的作品。",
      "chapterRange": [21, 28]
    }
  ],
  "challenges": [
    {
      "chapterNumber": 1,
      "title": "把課堂名片改成你的第一張網頁名片",
      "sessionHref": "sessions/01-web-files-tools/index.html",
      "level": "imitate",
      "gate": false,
      "brief": "沿用範例結構，換成你的姓名、身分與一個真正可點擊的聯絡方式，並能指出檔案、標籤與瀏覽器畫面的對應關係。",
      "summary": "換成自己的身份、介紹與聯絡入口，說出程式和畫面的對應。",
      "decisions": ["選擇想被記住的一句自我介紹", "決定聯絡連結使用 Email、社群或個人網站"],
      "antiCopy": "不得保留範例人物姓名、介紹句與聯絡網址；至少三處內容必須來自你自己。",
      "evidence": ["瀏覽器截圖可看見你的姓名、自我介紹與可辨識的聯絡入口", "口頭或文字標出 index.html 中三個內容分別對應到畫面的哪裡"],
      "hints": ["先只改文字，不要同時動標籤。", "用搜尋找到範例姓名，再逐一替換三個個人欄位。", "若連結沒有作用，檢查 href 是否包含完整 mailto: 或 https://。"],
      "solutionPolicy": "提供逐行對照範例；完成後必須換成個人內容才算通過。"
    },
    {
      "chapterNumber": 2,
      "title": "製作一頁興趣導覽",
      "sessionHref": "sessions/02-html-media-embed/index.html",
      "level": "imitate",
      "gate": false,
      "brief": "依照個人介紹頁範例，選一個你真的喜歡的主題，加入圖片、外部連結與一段可播放或可瀏覽的嵌入內容。",
      "summary": "用自己的主題安排圖片、外部連結與嵌入內容。",
      "decisions": ["決定興趣主題與三項最值得介紹的內容", "選擇圖片、影片或地圖最適合承擔的資訊"],
      "antiCopy": "不得使用範例的主題與媒體網址；每個媒體都要能說明它為何出現在頁面。",
      "evidence": ["頁面同時包含有意義 alt 的圖片與可開啟的外部連結", "嵌入內容正常顯示，且旁邊有一句說明觀看目的"],
      "hints": ["先用 h1、p、ul 排好純文字內容。", "圖片先放同一資料夾並使用相對路徑，確認顯示後再補 alt。", "嵌入失敗時先確認服務是否允許 iframe，再核對 embed 專用網址。"],
      "solutionPolicy": "提供標籤骨架與一組媒體範例，不提供學生主題的內容。"
    },
    {
      "chapterNumber": 3,
      "title": "替興趣頁建立專屬視覺語氣",
      "sessionHref": "sessions/03-css-foundations/index.html",
      "level": "imitate",
      "gate": false,
      "brief": "沿用 CSS 基礎範例，為上一章興趣頁建立一組符合主題的文字層級、顏色與閱讀寬度。",
      "summary": "自己選配色、文字層級與閱讀寬度。",
      "decisions": ["選擇一個主色與一個背景色並說明主題關聯", "決定標題、內文與輔助文字的尺寸層級"],
      "antiCopy": "不得照抄範例色碼與所有尺寸；至少調整五個 CSS 宣告並保留可讀性。",
      "evidence": ["截圖中能清楚辨識標題、內文與連結三種層級", "列出五個自行修改的屬性與改動前後差異"],
      "hints": ["先改 body 的背景與文字顏色。", "再用同一個選取器一次調整一類元素，不要逐標籤寫 style。", "若畫面雜亂，先限制內容 max-width，再檢查文字與背景的對比。"],
      "solutionPolicy": "提供 CSS 語法示範與可讀性檢核表，不提供完成配色。"
    },
    {
      "chapterNumber": 4,
      "title": "把內容整理成兩張資訊卡",
      "sessionHref": "sessions/04-box-model-selectors/index.html",
      "level": "imitate",
      "gate": false,
      "brief": "模仿卡片範例，把興趣頁的兩組資訊分成有內距、邊框與間距的卡片，避免內容黏在一起。",
      "summary": "依內容密度調整盒模型，而不是照抄固定數值。",
      "decisions": ["決定哪兩組資訊值得分成卡片", "決定卡片要用邊框、陰影或背景差異建立層級"],
      "antiCopy": "不得只複製 .card 樣式；需依內容密度自行調整 padding、margin 與寬度。",
      "evidence": ["DevTools 盒模型截圖能指出 content、padding、border、margin", "窄螢幕截圖中卡片沒有水平捲動或內容貼邊"],
      "hints": ["先替兩組內容加上相同 class。", "從 padding 開始讓內容離邊界有距離，再用 margin 分開卡片。", "尺寸不如預期時在 DevTools 檢查 box-sizing 與實際計算寬度。"],
      "solutionPolicy": "提供單張卡片的盒模型範例；第二張必須由學生自行套用與調整。"
    },
    {
      "chapterNumber": 5,
      "title": "設計一個有明確邀請的 Hero",
      "sessionHref": "sessions/05-hero-backgrounds/index.html",
      "level": "imitate",
      "gate": false,
      "brief": "依照 Hero 範例，為興趣頁製作首屏：一句主張、一句補充與一個引導訪客繼續看的行動。",
      "summary": "讓主張、補充與 CTA 對應真實頁面內容。",
      "decisions": ["決定訪客進站三秒內應理解的核心主張", "選擇背景圖、漸層或純色，並決定文字可讀性的處理方式"],
      "antiCopy": "不得沿用範例標題、背景與 CTA 文案；CTA 必須對應頁面內真實存在的內容。",
      "evidence": ["首屏截圖包含主張、補充與可點擊 CTA", "說明背景處理如何確保文字在桌機與手機都可讀"],
      "hints": ["先用純色背景完成文字與按鈕位置。", "再加入背景圖，使用 background-position 選擇焦點。", "文字看不清時加入 overlay，逐步調整透明度而不是直接換白底。"],
      "solutionPolicy": "提供 Hero 結構與 overlay 技法；文案、圖片焦點與 CTA 由學生決定。"
    },
    {
      "chapterNumber": 6,
      "title": "建立能走訪內容的導覽與雙欄區塊",
      "sessionHref": "sessions/06-flex-navbar-columns/index.html",
      "level": "imitate",
      "gate": false,
      "brief": "模仿 Flex 導覽列與兩欄範例，讓訪客可跳到頁面三個區塊，並把一段文字與一張圖片排成有主次的雙欄內容。",
      "summary": "讓三個入口可用，並自行決定圖片與文字比例。",
      "decisions": ["決定導覽列保留哪三個最重要入口", "決定雙欄中圖片與文字的比例及先後順序"],
      "antiCopy": "不得保留範例選單名稱；每個導覽 href 都要指向你頁面中真實且唯一的 id。",
      "evidence": ["錄影或連續截圖證明三個導覽連結都能抵達正確區塊", "標示 Flex 容器與兩個子項目，說明比例設定"],
      "hints": ["先替三個目標 section 加上唯一 id。", "讓 nav 變成 flex，再分別處理排列與間距。", "雙欄比例失控時檢查子項目的 flex-basis、min-width 與圖片 max-width。"],
      "solutionPolicy": "提供可運作的單列 Flex 範例；導覽目標與雙欄比例需自行完成。"
    },
    {
      "chapterNumber": 7,
      "title": "把固定作品卡改造成可擴充清單",
      "sessionHref": "sessions/07-grid-cards-overflow/index.html",
      "level": "modify",
      "gate": false,
      "brief": "從 Grid 範例出發，自訂至少四張內容不同的卡片，並讓欄數隨空間改變、長標題也不破版。",
      "summary": "建立四筆真實資料，讓欄數與長內容都能適應。",
      "decisions": ["決定卡片共同欄位與資訊優先順序", "選擇固定欄數或 auto-fit，並說明適用原因"],
      "antiCopy": "不得只換圖片；卡片資料、欄位順序與 Grid 規則至少各有一項實質改造。",
      "evidence": ["寬版與窄版截圖顯示四張卡會合理換欄", "以一張刻意加長標題的卡證明沒有溢出"],
      "hints": ["先複製卡片結構並換成四筆真實內容。", "使用 grid-template-columns 設定欄位，再縮放瀏覽器觀察斷點。", "內容溢出時同時檢查 grid 子項 min-width、圖片尺寸與文字換行。"],
      "solutionPolicy": "提供兩張卡與基本 Grid；學生需擴充資料並選擇響應式欄位策略。"
    },
    {
      "chapterNumber": 8,
      "title": "為卡片加入真正有用的浮動資訊",
      "sessionHref": "sessions/08-position-zindex/index.html",
      "level": "modify",
      "gate": false,
      "brief": "改造 position 範例，替卡片加入狀態標籤，並設計一個不遮住內容的回到頂端或聯絡浮動按鈕。",
      "summary": "依頁面情境設計狀態標籤與不遮內容的操作。",
      "decisions": ["決定什麼資訊值得使用絕對定位標籤", "選擇浮動按鈕在不同螢幕的安全位置"],
      "antiCopy": "不得使用無意義的 NEW 標籤；標籤與浮動操作都必須回應你的頁面情境。",
      "evidence": ["截圖顯示標籤相對卡片定位且不因頁面捲動漂移", "手機尺寸截圖證明固定按鈕不遮住主要 CTA 或文字"],
      "hints": ["先在卡片父層設定 position: relative。", "再把標籤設為 absolute，從 top 與 right 小幅調整。", "若元素互相遮蓋，畫出堆疊順序並檢查 positioning、z-index 與 stacking context。"],
      "solutionPolicy": "提供定位關係圖與單一 badge 範例；實際資訊與安全位置由學生驗證。"
    },
    {
      "chapterNumber": 9,
      "title": "建立可重複但有變化的元件家族",
      "sessionHref": "sessions/09-buttons-cards-components/index.html",
      "level": "modify",
      "gate": false,
      "brief": "把散落樣式整理成按鈕與卡片元件，至少做出主要、次要與停用三種按鈕狀態，以及兩種卡片變體。",
      "summary": "拆分共用規則與變體，做出多種狀態。",
      "decisions": ["定義哪個操作是主要行動、哪個是次要行動", "決定共用 class 與變體 class 的責任邊界"],
      "antiCopy": "不得為每一顆按鈕另寫一套 class；需證明共用規則與變體規則能組合。",
      "evidence": ["元件展示區同時呈現三種按鈕狀態與兩種卡片變體", "圈出重複刪除前後的 CSS，說明共用規則減少了什麼"],
      "hints": ["先找出按鈕之間完全相同的宣告。", "把相同宣告移到 .button，再用 modifier 只保留差異。", "若變體互相覆蓋，檢查 class 載入順序與選取器權重，不要立刻加 !important。"],
      "solutionPolicy": "提供命名範例與一組 primary/secondary；disabled 與卡片變體由學生完成。"
    },
    {
      "chapterNumber": 10,
      "title": "把裝飾動畫改造成操作回饋",
      "sessionHref": "sessions/10-transition-animation-cta/index.html",
      "level": "modify",
      "gate": false,
      "brief": "檢視頁面現有互動，選兩處加入能解釋狀態的 transition 或 animation，並支援偏好減少動態的使用者。",
      "summary": "只為有意義的狀態加入動態，並照顧 reduced motion。",
      "decisions": ["決定哪兩個狀態變化最需要視覺回饋", "選擇持續時間與移動幅度，使回饋清楚但不干擾閱讀"],
      "antiCopy": "不得為了展示而讓所有元素不停跳動；每個效果都要能回答「它告訴使用者什麼」。",
      "evidence": ["短錄影呈現 hover/focus 或狀態切換前後差異", "程式碼截圖顯示 prefers-reduced-motion 下已降低或關閉非必要動態"],
      "hints": ["先寫出互動前後兩個明確狀態。", "只 transition 會變化的屬性，避免直接使用 all。", "若動畫造成不適或卡頓，縮小位移、檢查 transform/opacity，並加入 reduced-motion 規則。"],
      "solutionPolicy": "提供一個按鈕 transition 與 reduced-motion 模板；第二個情境需自行設計。"
    },
    {
      "chapterNumber": 11,
      "title": "從症狀出發完成一次 CSS 偵錯",
      "sessionHref": "sessions/11-css-refactor-devtools/index.html",
      "level": "modify",
      "gate": false,
      "brief": "刻意挑一個目前頁面問題，以 DevTools 蒐集證據、提出假設、修正並整理重複 CSS，留下可重現的偵錯紀錄。",
      "summary": "留下症狀、證據、假設、修改與回歸的完整紀錄。",
      "decisions": ["選擇一個真實可觀察的版面或樣式症狀", "決定修正應改共用規則、變體規則或 HTML 結構"],
      "antiCopy": "不得只照著答案改數值；需先保留症狀截圖與至少一個被證據推翻的假設。",
      "evidence": ["一組修正前後截圖與可重現步驟", "偵錯紀錄包含症狀、檢查證據、假設、修改與回歸檢查"],
      "hints": ["先用一句話描述看得到的症狀，不要先寫原因。", "在 Computed 與 Styles 面板確認實際生效值及來源檔案。", "若第一次修改無效，列出 specificity、繼承、盒模型三個不同假設逐一排除。"],
      "solutionPolicy": "只提供偵錯紀錄格式與工具操作示範，不提供學生頁面的修正值。"
    },
    {
      "chapterNumber": 12,
      "title": "為一種真實裝置重新設計閱讀順序",
      "sessionHref": "sessions/12-rwd-layout-images-nav/index.html",
      "level": "modify",
      "gate": false,
      "brief": "選一種你常用的手機寬度，重新檢查導覽、Hero、雙欄與卡片，讓內容不是縮小而已，而是有適合窄螢幕的順序。",
      "summary": "依內容破版選斷點，主動調整導覽或資訊順序。",
      "decisions": ["決定手機版哪些內容要先出現、哪些可以後移或收合", "選擇斷點依內容破版位置，而非照抄裝置型號"],
      "antiCopy": "不得只加入單一 max-width 與全部改成一欄；至少有一處順序、導覽或圖片策略的主動改造。",
      "evidence": ["指定手機寬度的完整頁面截圖，沒有水平捲動", "列出三個斷點前後的設計決策與理由"],
      "hints": ["先在 DevTools 慢慢縮窄，記錄內容第一次擠壓的位置。", "從最影響閱讀的導覽與雙欄開始寫 media query。", "若圖片造成位移或溢出，檢查 width、height、object-fit 與容器 min-width。"],
      "solutionPolicy": "提供 RWD 檢查清單與一個 media query 範例；斷點與內容順序由學生決定。"
    },
    {
      "chapterNumber": 13,
      "title": "創作關卡一：交付一個不是課堂複製品的作品集",
      "sessionHref": "sessions/13-portfolio-integration/index.html",
      "level": "modify",
      "gate": true,
      "brief": "整合第 1–12 章能力，為你選定的身份與觀看者完成一頁作品集；在不看完成範例的情況下完成至少三個內容區塊與一個有效行動。",
      "summary": "為特定觀看者重組內容，完成有效 CTA 與同儕測試。",
      "decisions": ["定義作品集的主要觀看者與他最需要先知道的資訊", "從已學版型中選擇、刪除與重組元件，形成自己的內容順序", "決定一個可驗證的主要 CTA"],
      "antiCopy": "成品不得與課堂 solution 使用相同主題、文案、圖片組合與區塊順序；至少提出三項差異證據。",
      "evidence": ["桌機與手機完整頁面截圖，能辨認一致的視覺與清楚的閱讀順序", "一頁創作說明包含受眾、三項設計決策、取捨與 CTA 測試結果", "同學依指定任務操作後留下的回饋與一次修正紀錄"],
      "hints": ["先寫一句「這個網站要幫誰看見什麼」，再列出必要區塊。", "把必要區塊畫成方塊排序，只挑學過且能服務目的的元件。", "卡住時先拿掉無法說明用途的區塊，再用受眾任務逐段檢查內容、RWD 與 CTA。"],
      "solutionPolicy": "CREATE GATE：不提供單一完成版；只提供需求範例、空白規劃表、rubric 與同儕回饋問題。"
    },
    {
      "chapterNumber": 14,
      "title": "把作品交給一位沒看過課程的人接手",
      "sessionHref": "sessions/14-git-github-readme/index.html",
      "level": "transfer",
      "gate": false,
      "brief": "把作品建立版本紀錄並撰寫 README，讓另一位同學只看文件就能理解目的、開啟方式、目錄結構與下一步。",
      "summary": "用版本歷史與 README 讓同學能獨立開啟、理解。",
      "decisions": ["決定 commit 如何切分才能表達作品演進", "選擇 README 中對接手者最重要的安裝與驗證資訊"],
      "antiCopy": "不得使用只寫「我的作業」的 README 或一次提交全部檔案；文件必須對應你的真實專案。",
      "evidence": ["Git 歷史至少三個意義清楚且範圍合理的 commit", "同學只依 README 成功開啟作品並指出一項可改善之處"],
      "hints": ["先用 git status 分辨追蹤中、未追蹤與已修改檔案。", "每完成一個可說明的改變就寫一個動詞開頭的 commit。", "接手者卡住時，記錄他缺少的資訊並補到 README 的正確段落。"],
      "solutionPolicy": "提供 README 結構與 commit 好壞對照，不提供可直接貼上的專案描述。"
    },
    {
      "chapterNumber": 15,
      "title": "讓一份使用者輸入改變內容卡片",
      "sessionHref": "sessions/15-js-dom-events/index.html",
      "level": "transfer",
      "gate": false,
      "brief": "把 DOM 事件範例遷移到個人作品情境：讓使用者輸入或選擇一項資料後，畫面中的卡片內容或狀態立即改變。",
      "summary": "把 DOM 事件搬到符合自己作品目的的小任務。",
      "decisions": ["決定使用者要輸入或選擇什麼，改變哪個畫面結果", "選擇 click、input 或 change 事件並說明時機"],
      "antiCopy": "不得只做課堂同款改標題按鈕；輸入、事件與結果必須形成符合頁面目的的小任務。",
      "evidence": ["錄影呈現初始狀態、使用者操作與更新後狀態", "流程圖標出事件來源、讀取資料、處理與 DOM 更新四步"],
      "hints": ["先用一句話寫出「當誰做什麼，畫面要如何改變」。", "分別取得輸入元素與結果元素，再綁定一個事件。", "沒有反應時檢查 selector 是否找到元素、事件是否觸發、資料是否真的寫回 DOM。"],
      "solutionPolicy": "提供事件處理器空骨架與 console 驗證方式；學生自行定義資料與畫面結果。"
    },
    {
      "chapterNumber": 16,
      "title": "設計一個會記住選擇的偏好設定",
      "sessionHref": "sessions/16-js-logic-storage/index.html",
      "level": "transfer",
      "gate": false,
      "brief": "將條件判斷、陣列與 localStorage 應用到新情境，例如閱讀模式、收藏主題或內容篩選，重新開頁仍保留選擇。",
      "summary": "用條件、資料與 localStorage 保存真實偏好。",
      "decisions": ["決定要保存的偏好資料型態與預設值", "定義不同選擇如何改變畫面，以及資料不存在時如何處理"],
      "antiCopy": "不得直接製作與範例相同的計數器；需使用至少一個布林、字串或陣列資料完成真實偏好。",
      "evidence": ["重新整理前後的操作錄影，證明偏好仍存在", "Application 面板截圖與資料結構說明，包含預設值處理"],
      "hints": ["先只用變數與 if 完成畫面切換。", "再把選擇用 JSON.stringify 寫入 localStorage，載入時讀回。", "資料讀取失敗時檢查 key 是否一致、JSON 是否有效，並為 null 或舊格式提供 fallback。"],
      "solutionPolicy": "提供 localStorage 讀寫函式範例，不提供完整偏好功能。"
    },
    {
      "chapterNumber": 17,
      "title": "把輪播技術轉成內容推薦器",
      "sessionHref": "sessions/17-carousel-render-random/index.html",
      "level": "transfer",
      "gate": false,
      "brief": "不做一般圖片輪播，改用陣列與 render 建立符合你主題的推薦器，可下一筆、上一筆或隨機取得內容。",
      "summary": "自訂資料欄位，以 render 與索引呈現主題推薦。",
      "decisions": ["定義每筆推薦資料需要哪些欄位", "決定順序瀏覽或隨機推薦更符合使用者目的"],
      "antiCopy": "不得沿用範例圖片與文字；至少新增一個圖片輪播以外的資料欄位與顯示規則。",
      "evidence": ["功能錄影顯示至少五筆資料可切換且索引不越界", "一張資料物件與 render 畫面欄位的對照圖"],
      "hints": ["先建立兩筆資料並讓 render(0) 正確顯示。", "再加入索引更新與邊界處理，最後擴充到五筆。", "出現 undefined 時同時檢查目前索引、陣列長度與物件欄位名稱。"],
      "solutionPolicy": "提供 render 單筆資料與索引演算提示；推薦內容與完整控制流程由學生完成。"
    },
    {
      "chapterNumber": 18,
      "title": "為一個生活流程設計可暫停的計時狀態",
      "sessionHref": "sessions/18-timers-state/index.html",
      "level": "transfer",
      "gate": false,
      "brief": "選擇專注、沖煮、伸展或其他生活流程，設計開始、暫停、繼續、重設與完成狀態，而非只複製倒數計時器。",
      "summary": "從生活情境推導狀態、轉換與重複計時防護。",
      "decisions": ["定義流程需要哪些狀態與合法轉換", "決定完成、暫停與重設時給使用者什麼回饋"],
      "antiCopy": "不得只改倒數秒數；必須有符合情境的狀態名稱、至少一項額外回饋與重複計時防護。",
      "evidence": ["狀態圖涵蓋 idle、running、paused、finished 及轉換條件", "錄影證明連按開始不會加速，暫停與重設結果正確"],
      "hints": ["先畫狀態與按鈕，不要先寫 setInterval。", "只允許 running 狀態建立計時器，保存 interval id。", "時間異常時檢查是否建立多個 interval、是否在暫停/完成 clear，以及畫面是否由單一狀態渲染。"],
      "solutionPolicy": "提供狀態機表格與計時器生命週期片段，不提供情境完整程式。"
    },
    {
      "chapterNumber": 19,
      "title": "接手一個陌生 FAQ 並新增可維護需求",
      "sessionHref": "sessions/19-jquery-faq-legacy/index.html",
      "level": "transfer",
      "gate": false,
      "brief": "模擬接手舊站：先閱讀 jQuery FAQ，新增「一次只開一題」與鍵盤可操作需求，再用最小改動完成並記錄風險。",
      "summary": "讀懂舊碼後以最小修改加入開合與鍵盤需求。",
      "decisions": ["決定修改既有事件處理或新增獨立初始化邏輯", "決定如何用 class、aria-expanded 表達開合狀態"],
      "antiCopy": "不得整份改寫成新框架；需保留現有行為並說明每個修改為何是最小範圍。",
      "evidence": ["變更前後測試表涵蓋點擊、鍵盤、重複點擊與多題狀態", "diff 截圖與接手筆記說明舊碼入口、修改點及回歸風險"],
      "hints": ["先不修改程式，找出事件綁定與控制顯示的 class。", "在現有 click 流程前後記錄 this、目前開啟項目與 aria 狀態。", "新需求破壞舊功能時，用測試表逐項回退，確認是事件冒泡、選取範圍或狀態同步問題。"],
      "solutionPolicy": "提供舊碼閱讀問題與測試案例，不提供完整修正版。"
    },
    {
      "chapterNumber": 20,
      "title": "創作關卡二：把陌生 API 變成有用的小工具",
      "sessionHref": "sessions/20-api-json-async/index.html",
      "level": "transfer",
      "gate": true,
      "brief": "自行選擇一個公開 API，先閱讀回傳資料，再為特定使用者任務製作載入、成功、空資料與錯誤四種狀態的小工具。",
      "summary": "自選 API 與任務，處理載入、成功、空資料、錯誤。",
      "decisions": ["選擇 API 與一個明確使用者任務，而非展示全部欄位", "決定從 JSON 中保留哪些資料及如何轉成使用者看得懂的介面", "定義網路失敗與空結果時下一步"],
      "antiCopy": "不得使用課堂同款 API、同款卡片或只把 JSON 全部印出；必須有資料篩選與情境化文案。",
      "evidence": ["四種 UI 狀態截圖或錄影：載入、成功、空資料、錯誤", "API 文件連結、資料欄位對照與 async 流程圖", "一位同學完成指定任務的觀察紀錄與一次修正"],
      "hints": ["先在瀏覽器或 API 工具看一筆真實回傳，圈出任務需要的欄位。", "以假資料完成四種 UI 狀態，再接 fetch 並切換狀態。", "失敗時分別檢查 URL/CORS、HTTP status、JSON 結構與 render，保留 console 或 Network 證據。"],
      "solutionPolicy": "CREATE GATE：提供 API 評估表、狀態規格與 fetch 最小片段，不提供任何一個可直接交作業的完整工具。"
    },
    {
      "chapterNumber": 21,
      "title": "設計一份能被網站正確使用的資料表",
      "sessionHref": "sessions/21-gas-sheet-api/index.html",
      "level": "create",
      "gate": false,
      "brief": "先選擇一個要維護的公開資料集合，自行定義試算表欄位、資料規則與 GAS 輸出，再讓網站讀取至少五筆資料。",
      "summary": "自訂資料 schema、測試資料與 GAS 輸出規則。",
      "decisions": ["定義每列代表什麼實體及哪些欄位必填", "決定 API 要公開哪些欄位、如何處理空值與型別"],
      "antiCopy": "不得沿用課堂商品欄位；資料主題、schema 與至少五筆內容必須由學生定義。",
      "evidence": ["試算表 schema 表含欄位名稱、型別、必填與範例值", "GAS JSON 回應與網頁渲染截圖能對上至少五筆資料"],
      "hints": ["先用紙寫出一筆資料，再找出五筆都會重複的欄位。", "在試算表放入五筆刻意包含空值或不同型別的測試資料。", "前端欄位變 undefined 時，逐層比對表頭、GAS 輸出 key、Network response 與 render 使用的名稱。"],
      "solutionPolicy": "提供通用表格轉 JSON 範本；資料 schema 與前端呈現不提供完整解。"
    },
    {
      "chapterNumber": 22,
      "title": "為自己的資料設計搜尋、篩選或分頁",
      "sessionHref": "sessions/22-product-list-pagination/index.html",
      "level": "create",
      "gate": false,
      "brief": "延續自訂資料集，依使用者真正需要找資料的方式，選擇搜尋、分類篩選或分頁至少兩項並自行定義互動規則。",
      "summary": "根據真實任務選操作組合，處理空結果與邊界。",
      "decisions": ["根據資料量與任務選擇搜尋、篩選、排序、分頁的組合", "決定操作後目前頁碼、空結果與結果數如何回饋"],
      "antiCopy": "不得直接套商品列表的固定每頁筆數與分類；互動規則需來自你的資料特性。",
      "evidence": ["至少三個測試案例涵蓋一般結果、空結果與邊界頁碼", "流程說明顯示原始資料如何依序經過篩選、切頁與 render"],
      "hints": ["先寫三個使用者會怎麼找資料的例句。", "把 filter、paginate、render 拆成各自可測的函式。", "結果或頁碼錯誤時固定同一組資料，逐步印出每個函式輸入輸出並確認操作後是否重設頁碼。"],
      "solutionPolicy": "提供純函式介面與測試資料，不提供符合學生 schema 的完整實作。"
    },
    {
      "chapterNumber": 23,
      "title": "建立一份有人願意填完的申請表",
      "sessionHref": "sessions/23-form-post-validation/index.html",
      "level": "create",
      "gate": false,
      "brief": "為活動報名、合作詢問、預約或自選情境設計表單，讓欄位、驗證、錯誤訊息與送出後回饋都服務同一任務。",
      "summary": "從情境推導最少欄位、驗證、錯誤與送出回饋。",
      "decisions": ["決定完成任務真正需要的最少欄位與蒐集理由", "定義每個欄位的前端驗證、錯誤文案與送出後狀態"],
      "antiCopy": "不得沿用姓名、Email、訊息三欄模板直接交付；至少有一個情境專屬欄位與對應驗證。",
      "evidence": ["欄位規格表含目的、必填、格式、錯誤訊息與隱私考量", "錄影涵蓋無效輸入、修正、送出中、成功或失敗流程"],
      "hints": ["先刪到只剩完成任務不可缺少的欄位。", "逐欄寫下有效與無效範例，再把規則實作成驗證。", "送出結果不一致時檢查 preventDefault、payload、Network request 與按鈕狀態是否完整復原。"],
      "solutionPolicy": "提供表單狀態骨架與驗證函式範例；欄位、文案與 POST payload 由學生設計。"
    },
    {
      "chapterNumber": 24,
      "title": "設計可訂閱的共享資料生命週期原型",
      "sessionHref": "sessions/24-firebase-realtime/index.html",
      "level": "create",
      "gate": false,
      "brief": "選一個需要多個畫面區塊共享更新的情境，自行定義資料生命週期，使用 memory adapter 完成同頁兩個 subscriber、unsubscribe、安全渲染與失敗後 retry 的可操作原型。",
      "summary": "自訂共享資料生命週期、訂閱解除與安全失敗恢復。",
      "decisions": ["決定共享資料的欄位、唯一 id 與建立到失效的生命週期", "設計兩個 subscriber 各自負責的畫面資訊與 unsubscribe 時機", "定義通知或寫入失敗後保留什麼狀態、何時允許 retry"],
      "antiCopy": "不得使用課堂同款聊天室或 TODO，也不得只改 subscriber 名稱；資料情境、兩個訂閱畫面與失敗恢復規則都必須自行設計。",
      "evidence": ["同頁兩個 subscriber 收到同一筆更新，unsubscribe 其中一個後只有另一個繼續更新", "輸入 XSS 測試字串後只顯示原始文字，沒有產生危險節點或執行腳本", "模擬通知或寫入失敗後資料仍可追蹤，retry 成功且不產生重複項目", "資料生命週期圖標出 adapter、事件、subscriber、unsubscribe 與錯誤恢復"],
      "hints": ["先用一份記憶體資料與兩個純文字畫面區塊，證明一次事件能通知兩個 subscriber。", "再加入 unsubscribe，讓其中一個區塊停止接收後續事件；渲染一律使用 textContent 等安全 DOM API。", "最後注入一次失敗，記錄事件 id、錯誤狀態與 retry 次數，確認恢復後不重複通知。"],
      "solutionPolicy": "標準挑戰只核對 memory adapter、同頁 subscriber、unsubscribe、XSS 安全與 retry。真 Firebase 雙視窗屬另做的 live extension，必須另備服務設定、網路與安全規則，並由教師或同儕人工驗真；標準原型不得宣稱已證明正式即時同步。"
    },
    {
      "chapterNumber": 25,
      "title": "用 mock 身分狀態驗證權限矩陣",
      "sessionHref": "sessions/25-auth-uid-permissions/index.html",
      "level": "create",
      "gate": false,
      "brief": "延續共享資料原型，自訂一種可編輯資源，先寫 owner、guest、unauth 對單一編輯操作的權限矩陣，再用 mock auth state、uid 與 canEdit 證明 owner 成功，以及 guest、unauth 兩條拒絕路徑。",
      "summary": "用 mock owner、guest、unauth 驗證單一編輯權限與兩條拒絕路徑。",
      "decisions": ["定義自訂資源、ownerId 與單一編輯操作允許成立的條件", "分別決定 guest、unauth 被拒絕時顯示什麼原因且不洩漏受保護內容", "設計 auth state、uid、ownerId 與 canEdit 的判斷流程及身分切換方式"],
      "antiCopy": "不得只隱藏編輯按鈕或照抄課堂資料；編輯事件本身也要經過 mock canEdit 判斷，並以自己設計的資源驗證三種身分。",
      "evidence": ["權限矩陣只聚焦單一編輯操作，列出 owner、guest、unauth 的允許或拒絕結果與理由", "測試紀錄顯示 owner 成功編輯，以及 guest、unauth 各自被拒絕的兩條路徑", "狀態追蹤能對上目前 auth state、uid、資源 ownerId、canEdit 與畫面回饋"],
      "hints": ["先畫只有一個「編輯」欄位的三列矩陣，不要擴張成完整 CRUD 或正式規則語法。", "把編輯條件寫成只回傳 true/false 的 canEdit，按鈕狀態與編輯事件共用同一判斷。", "拒絕失效時逐步比對 auth state、uid、ownerId、canEdit 與事件處理，不要只檢查按鈕是否可見。"],
      "solutionPolicy": "標準挑戰只提供 mock auth state 與單一編輯權限判斷骨架，不證明正式 Rules 已生效。read／create／delete 的完整 CRUD 權限矩陣僅列為概念延伸；正式 Auth／Rules 則屬 live extension，需另接服務與人工驗證。"
    },
    {
      "chapterNumber": 26,
      "title": "從一個日常痛點設計本機 CRUD 原型",
      "sessionHref": "sessions/26-todo-local-crud/index.html",
      "level": "create",
      "gate": false,
      "brief": "不限 TODO，從你或身邊人的日常痛點選一個資料實體，自己定義新增、查看、修改、刪除及保存規則，完成可操作的本機原型。",
      "summary": "不限 TODO；自訂問題、資料與操作，不提供完整解。",
      "decisions": ["選擇真正需要被管理的資料實體與核心使用者", "定義 CRUD 各動作的觸發、確認、失敗與空狀態", "決定資料欄位、唯一 id 與 localStorage 更新策略"],
      "antiCopy": "不得只把 TODO 換名；資料欄位、主要操作與畫面資訊架構至少兩項需與 TODO 不同。",
      "evidence": ["問題訪談或觀察筆記，能連結到資料欄位與核心操作", "CRUD 測試錄影涵蓋新增、重新整理、修改、刪除、空資料與錯誤輸入", "資料模型與狀態流程圖可對上實際程式"],
      "hints": ["先用一句「某人現在很難完成某事」定義痛點，再只選一個資料實體。", "先用記憶體陣列完成 create 與 render，再依序補 update、delete，最後才接 localStorage。", "資料不同步時以唯一 id 追蹤一次操作，逐步比對事件輸入、陣列、儲存內容與 render 結果。"],
      "solutionPolicy": "不提供完整 solution；只提供 CRUD 驗收清單、資料模型範例與分層提示，學生需自行完成架構與介面。"
    },
    {
      "chapterNumber": 27,
      "title": "讓自選作品離線排隊並能重試恢復",
      "sessionHref": "sessions/27-todo-gas-sync/index.html",
      "level": "create",
      "gate": false,
      "brief": "為第 26 章自選作品設計可持久保存的 queue，定義本機操作如何入列、offline 或送出失敗時如何保留，以及恢復後如何 retry；可選擇再接 append-only GAS POST。",
      "summary": "自訂持久 queue、offline 保留、retry 與送出前 preflight。",
      "decisions": ["決定哪些本機操作要入列，以及 queue item 的唯一 id、payload 與狀態欄位", "定義 offline、sending、failed、retrying、synced 的轉換與使用者恢復操作", "選擇只做 mock adapter 或加做 append-only GAS POST，並設計自選 schema 的 preflight 與欄位映射"],
      "antiCopy": "不得只替換課堂 GAS URL；必須為自選 schema 設計可持久 queue、preflight，並實作 offline 或錯誤 endpoint 下的保留與 retry。",
      "evidence": ["重新整理後 pending queue 仍存在；offline 或錯誤 endpoint 時失敗項保留，恢復後 retry 只移除成功項", "preflight 報告、queue payload 與畫面狀態能以唯一 id 對上同一次操作", "若選做 append-only GAS POST，提供實際 request／response 與 Sheet 新增列的欄位對照；未接正式服務則清楚標示 mock 證據"],
      "hints": ["先讓每次本機操作形成含唯一 id 的 queue item，保存後重新整理確認仍能讀回。", "用 mock adapter 切換 offline／失敗／成功，確認 failed item 不會被清掉，retry 只重送仍待處理項目。", "選做 GAS 時只做 append-only POST，先跑 preflight 再比對 request payload、response 與 Sheet 新增列，不加入遠端讀回。"],
      "solutionPolicy": "不提供完整 solution；只提供持久 queue、offline／retry 狀態契約、preflight 與可選 append-only GAS POST 介面。遠端讀回、雙向同步與衝突合併屬進階延伸，不列入標準創作挑戰。"
    },
    {
      "chapterNumber": 28,
      "title": "創作關卡三：公開展示你定義、完成並驗證的作品",
      "sessionHref": "sessions/28-netlify-showcase/index.html",
      "level": "create",
      "gate": true,
      "brief": "自行選擇一個真實問題、明確受眾與核心功能，完成可公開操作的作品並展示。非 TODO 也可以；應以受眾任務與可驗證成果決定功能，而非跟著課堂成品。",
      "summary": "自選問題、受眾與功能，非 TODO 也可以；不提供完整解。",
      "decisions": ["自選一個非 TODO 也可以的真實問題，界定受眾與成功任務", "從所有學過能力中選擇必要功能，主動刪除無助於核心任務的功能", "決定展示故事、操作路徑、公開網址與回饋收集方式", "定義作品目前限制與下一版優先改善項目"],
      "antiCopy": "不得用換色、換字或 TODO 改名充當創作；問題、受眾、資料、功能組合與展示敘事必須由學生自行定義，並能提出選擇證據。",
      "evidence": ["公開可開啟的網址與最新版 commit，手機及桌機均可完成核心任務", "三分鐘展示包含問題、受眾、操作、關鍵技術決策、失敗與修正", "至少兩位目標受眾的任務測試紀錄與依回饋完成的一次修改", "作品說明列出資料流程、功能範圍、已知限制與下一步"],
      "hints": ["先訪問一位可能使用的人，將問題寫成「誰在什麼情境下，想完成什麼卻遇到什麼阻礙」。", "把想做的功能分成核心任務必需、可延後、不要做三類，只完成能形成閉環的最小版本。", "展示前從全新瀏覽器依受眾任務走一遍，逐項檢查載入、空資料、錯誤、RWD、權限、公開網址與回復方式。"],
      "solutionPolicy": "FINAL CREATE GATE：不提供完整 solution、半成品專案或固定題目；只提供需求拆解表、四級 rubric、展示檢核與分層提示。"
    }
  ]
}
