{
  "formatVersion": 1,
  "name": "Coastal colours",
  "icon": "palette",
  "tagline": "Find a little seaside calm.",
  "prompt": "Which shade would you bring home?",
  "options": [
    {
      "id": "hide-names",
      "label": "Hide names",
      "description": "Choose with your eyes during comparisons.",
      "default": false,
      "effect": {
        "type": "hide-labels"
      }
    },
    {
      "id": "include-sea",
      "label": "Sea shades",
      "description": "Include shades tagged sea.",
      "default": true,
      "effect": {
        "type": "include-tag",
        "tag": "sea"
      }
    }
  ],
  "extensions": [
    {
      "name": "Colour studio",
      "description": "Explore list colours, rank favourites and copy colour codes from play cards.",
      "config": {
        "colourField": "colour"
      },
      "package": {
        "apiVersion": 1,
        "experimental": true,
        "capabilities": [
          "clipboard.write",
          "order.save"
        ],
        "surfaces": [
          {
            "id": "colour-code",
            "label": "Colour code",
            "slot": "card-caption",
            "height": 60
          },
          {
            "id": "spectrum",
            "label": "Explore colours",
            "slot": "tool",
            "height": 540
          }
        ],
        "script": "function mount(tilt) {\n    const root = tilt.root, ui=tilt.ui;\n    const element = (tag, text, parent = root) => { const el = document.createElement(tag); if (text) el.textContent = text; parent.append(el); return el; };\n    const colour = item => item.fields?.[tilt.context.config.colourField || 'colour'] || item.swatch;\n    const safeColour = value => typeof value === 'string' && /^#[a-f0-9]{6}$/i.test(value);\n    const rgb = hex => [1,3,5].map(n => parseInt(hex.slice(n,n+2),16));\n    if (tilt.surface.slot === 'card-caption') {\n      const draw = () => {\n        const item = tilt.context.items.find(i => i.id === tilt.context.pair[tilt.surface.side === 'left' ? 0 : 1]);\n        const hex = item && colour(item);\n        tilt.actions(safeColour(hex) ? [{id:'copy',label:hex.toUpperCase(),title:'Copy '+hex.toUpperCase(),variant:'subtle',command:{type:'clipboard.write',text:hex.toUpperCase()}}] : []);\n      };\n      tilt.onContext(draw); draw(); return;\n    }\n    root.className='tilt-stack';root.style.padding='4px';\n    const items=tilt.context.items.filter(i=>safeColour(colour(i)));\n    const hsl=item=>{\n      const [r,g,b]=rgb(colour(item)).map(v=>v/255),max=Math.max(r,g,b),min=Math.min(r,g,b),d=max-min,l=(max+min)/2;\n      let h=!d?0:max===r?((g-b)/d+6)%6:max===g?(b-r)/d+2:(r-g)/d+4;\n      return {h:h*60,s:d?d/(1-Math.abs(2*l-1)):0,l};\n    };\n    const neutrals=items.filter(item=>{const channels=rgb(colour(item));return hsl(item).s<.15 || (Math.max(...channels)-Math.min(...channels))/255<.12;}).sort((a,b)=>hsl(b).l-hsl(a).l);\n    const chromatic=items.filter(item=>!neutrals.includes(item));\n    // Balance the available shades into equal-width sectors and common rings.\n    // Exact divisors avoid inventing colours or duplicating the 44-shade starter palette.\n    const rings=[4,3,2,1].find(n=>chromatic.length%n===0&&chromatic.length/n<=16)||Math.min(4,chromatic.length)||1;\n    const sectors=Math.ceil(chromatic.length/rings),ordered=[...chromatic].sort((a,b)=>hsl(a).h-hsl(b).h||hsl(b).l-hsl(a).l);\n    const families=Array.from({length:sectors},(_,i)=>ordered.slice(i*rings,(i+1)*rings).sort((a,b)=>hsl(b).l-hsl(a).l));\n    const cells=[],tau=Math.PI*2,center=150,radius=142;\n    const polar=(r,a)=>[center+Math.sin(a)*r,center-Math.cos(a)*r];\n    families.forEach((family,i)=>Array.from({length:rings},(_,j)=>{\n      const item=family[Math.min(j,family.length-1)],start=tau*i/sectors,end=tau*(i+1)/sectors,inner=radius*j/rings,outer=radius*(j+1)/rings;\n      const angle=(start+end)/2,r=inner===0?outer*.75:(inner+outer)/2;\n      cells.push({item,start,end,inner,outer,x:polar(r,angle)[0],y:polar(r,angle)[1]});\n    }));\n    let picks=Array.isArray(tilt.state)?[...new Set(tilt.state.filter(id=>items.some(i=>i.id===id)))]:[];\n    let selected=Math.max(0,picks.length-1),dragging=-1;\n    element('p','Tap colours in preference order: 1 is your favourite. Reorder them below, then save.').className='tilt-description';\n    const plane=element('div');plane.style.cssText='width:min(100%,280px);aspect-ratio:1;align-self:center;position:relative;touch-action:none';\n    const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 300 300');svg.setAttribute('role','img');svg.setAttribute('aria-label','Colour wheel with '+chromatic.length+' list shades');svg.style.cssText='display:block;width:100%;height:100%';plane.append(svg);\n    cells.forEach(c=>{\n      const path=document.createElementNS(svg.namespaceURI,'path'),p1=polar(c.outer,c.start),p2=polar(c.outer,c.end),p3=polar(c.inner,c.end),p4=polar(c.inner,c.start);\n      // Split full circles into two arcs, so a one-family list works too.\n      const mid=(c.start+c.end)/2,pm=polar(c.outer,mid),qm=polar(c.inner,mid);\n      path.setAttribute('d','M'+p1+' A'+c.outer+','+c.outer+' 0 0 1 '+pm+' A'+c.outer+','+c.outer+' 0 0 1 '+p2+' L'+p3+(c.inner?' A'+c.inner+','+c.inner+' 0 0 0 '+qm+' A'+c.inner+','+c.inner+' 0 0 0 '+p4:'')+' Z');\n      path.setAttribute('fill',colour(c.item));path.setAttribute('stroke','var(--color-cream)');path.setAttribute('stroke-width','.6');\n      const title=document.createElementNS(svg.namespaceURI,'title');title.textContent=c.item.label+' '+colour(c.item).toUpperCase();path.append(title);svg.append(path);\n    });\n    const markers=element('div',null,plane);markers.style.cssText='position:absolute;inset:0;pointer-events:none';\n    const neutralRow=ui.layout('row');root.append(neutralRow);\n    const toolbar=ui.layout('row');root.append(toolbar);\n    const choices=ui.layout('stack');choices.setAttribute('aria-label','Your colour ranking');choices.style.cssText='max-height:220px;overflow:auto;gap:4px';root.append(choices);\n    const details=document.createElement('details');root.append(details);\n    const hint=element('p');hint.className='tilt-description';hint.style.margin='0';\n    const actions=()=>{\n      tilt.setState(picks);\n      tilt.actions(picks.length>=2?[{id:'save',label:'Save ranking',title:'Save this order as your estimated ranking',command:{type:'order.save',ids:picks}}]:[]);\n      hint.textContent=picks.length>=2?'Save this order as estimated scores, then refine it by comparing.':'Choose at least two colours to save a ranking.';\n    };\n    const move=(from,to)=>{to=Math.max(0,Math.min(picks.length-1,to));const [id]=picks.splice(from,1);picks.splice(to,0,id);selected=to;render();actions();choices.children[to]?.querySelector('button')?.focus();};\n    const replace=id=>{const existing=picks.indexOf(id);if(existing>=0)picks[existing]=picks[selected];picks[selected]=id;};\n    const add=id=>{const existing=picks.indexOf(id);if(existing>=0)selected=existing;else{picks.push(id);selected=picks.length-1;}render();actions();};\n    const render=()=>{\n      markers.replaceChildren();choices.replaceChildren();toolbar.replaceChildren();neutralRow.replaceChildren();details.replaceChildren();details.hidden=!picks[selected];\n      neutrals.forEach(item=>{const index=picks.indexOf(item.id);const button=ui.button({label:index<0?item.label:(index+1)+'. '+item.label,variant:'subtle',title:'Select '+item.label,onClick:()=>add(item.id)});button.setAttribute('aria-pressed',String(index>=0));const swatch=document.createElement('span');swatch.style.cssText='width:16px;height:16px;flex:none;border:1px solid var(--color-line);border-radius:50%;background:'+colour(item);button.prepend(swatch);neutralRow.append(button);});\n      picks.forEach((id,i)=>{\n        const item=items.find(item=>item.id===id),c=cells.find(c=>c.item.id===id);if(!item)return;\n        if(c){const dot=element('span',String(i+1),markers),size=c.inner===0?16:26;\n          dot.style.cssText='position:absolute;width:'+size+'px;height:'+size+'px;display:grid;place-items:center;color:white;text-shadow:0 1px 3px #000;font-size:'+(size===16?9:11)+'px;font-weight:900;border:2px solid white;border-radius:50%;transform:translate(-50%,-50%);box-shadow:0 0 0 '+(selected===i?'3px':'1px')+' #30283e88;left:'+(c.x/3)+'%;top:'+(c.y/3)+'%;background:'+colour(item);}\n        const row=ui.layout('row');row.style.cssText='flex-wrap:nowrap;gap:2px;border-radius:12px;padding:2px;background:'+(selected===i?'var(--color-lavender-soft)':'var(--color-surface)');choices.append(row);\n        const marker=ui.button({label:(i+1)+'. '+item.label,variant:'subtle',title:'Select '+item.label,onClick:()=>{selected=i;render();}});\n        marker.style.cssText='flex:1;min-width:0;justify-content:flex-start;text-align:left';\n        marker.setAttribute('aria-label','Colour '+(i+1)+': '+item.label+'. Use up and down arrows to rank; Delete to remove');marker.setAttribute('aria-pressed',String(selected===i));\n        const swatch=document.createElement('span');swatch.style.cssText='display:block;flex:none;width:16px;height:16px;border-radius:50%;border:1px solid var(--color-line);background:'+colour(item);marker.prepend(swatch);row.append(marker);\n        marker.onkeydown=e=>{if(e.key==='Delete'||e.key==='Backspace'){e.preventDefault();picks.splice(i,1);selected=Math.max(0,i-1);render();actions();choices.children[selected]?.querySelector('button')?.focus();}else if(e.key==='ArrowUp'||e.key==='ArrowDown'){e.preventDefault();move(i,i+(e.key==='ArrowUp'?-1:1));}};\n        for(const [label,title,offset] of [['\u2191','Move '+item.label+' up',-1],['\u2193','Move '+item.label+' down',1]]){const button=ui.button({label,variant:'subtle',title,disabled:i+offset<0||i+offset>=picks.length,onClick:()=>move(i,i+offset)});button.setAttribute('aria-label',title);row.append(button);}\n        const remove=ui.button({label:'\u00d7',variant:'subtle',title:'Remove '+item.label,onClick:()=>{picks.splice(i,1);selected=Math.max(0,Math.min(selected,picks.length-1));render();actions();}});remove.setAttribute('aria-label','Remove '+item.label);row.append(remove);\n      });\n      toolbar.append(ui.button({label:'Add colour',disabled:picks.length>=items.length,onClick:()=>{const next=items.find(item=>!picks.includes(item.id));if(next)add(next.id);}}));\n      const count=element('span',picks.length+' ranked',toolbar);count.className='tilt-description';count.style.marginLeft='auto';\n      if(picks[selected]){element('summary','Change selected shade',details).style.cssText='cursor:pointer;font-size:12px;font-weight:700;color:var(--color-ink-soft)';details.append(ui.field({label:'Selected shade',type:'select',value:picks[selected],choices:items.map(item=>({value:item.id,label:item.label+' \u00b7 '+colour(item).toUpperCase()})),onChange:id=>{replace(id);render();actions();}}));}\n    };\n    const position=e=>{const r=plane.getBoundingClientRect();return {x:(e.clientX-r.left)/r.width*300,y:(e.clientY-r.top)/r.height*300};};\n    const at=p=>{const dx=p.x-center,dy=p.y-center,r=Math.hypot(dx,dy),angle=(Math.atan2(dx,-dy)+tau)%tau;return cells.find(c=>angle>=c.start&&angle<c.end&&r>=c.inner&&r<=c.outer);};\n    plane.onpointerdown=e=>{\n      if(tilt.context.disabled)return;const p=position(e),c=at(p);if(!c)return;\n      dragging=picks.indexOf(c.item.id);\n      if(dragging<0){const existing=picks.indexOf(c.item.id);if(existing>=0)dragging=existing;else if(picks.length<items.length){picks.push(c.item.id);dragging=picks.length-1;}}\n      if(dragging<0)return;selected=dragging;plane.setPointerCapture(e.pointerId);render();actions();\n    };\n    plane.onpointermove=e=>{if(dragging<0)return;const c=at(position(e));if(c&&picks[dragging]!==c.item.id){selected=dragging;replace(c.item.id);render();}};\n    plane.onpointerup=plane.onpointercancel=()=>{dragging=-1;actions();};\n    tilt.onContext(()=>{render();actions();});render();actions();\n  }"
      }
    }
  ],
  "items": [
    {
      "id": "lagoon",
      "label": "Lagoon",
      "swatch": "#488F94",
      "tags": [
        "sea"
      ],
      "fields": {
        "colour": "#488F94"
      }
    },
    {
      "id": "deep-water",
      "label": "Deep water",
      "swatch": "#224D65",
      "tags": [
        "sea"
      ],
      "fields": {
        "colour": "#224D65"
      }
    },
    {
      "id": "sand",
      "label": "Warm sand",
      "swatch": "#E9D7BA",
      "tags": [
        "shore"
      ],
      "fields": {
        "colour": "#E9D7BA"
      }
    },
    {
      "id": "shell",
      "label": "Shell pink",
      "swatch": "#E5B8AC",
      "tags": [
        "shore"
      ],
      "fields": {
        "colour": "#E5B8AC"
      }
    }
  ],
  "fields": [
    {
      "id": "colour",
      "label": "Colour",
      "type": "color"
    }
  ],
  "presentation": {
    "colour": "colour"
  }
}
