{
 "version": "3.2.0",
 "count": 100,
 "games": 26,
 "multiplayer": 31,
 "categories": [
  {
   "id": "time",
   "label": "Time",
   "count": 7
  },
  {
   "id": "data",
   "label": "Live data",
   "count": 17
  },
  {
   "id": "media",
   "label": "Media",
   "count": 7
  },
  {
   "id": "creative",
   "label": "Creative",
   "count": 14
  },
  {
   "id": "pets",
   "label": "Pets",
   "count": 3
  },
  {
   "id": "games",
   "label": "Games",
   "count": 26
  },
  {
   "id": "casino",
   "label": "Casino",
   "count": 9
  },
  {
   "id": "ambient",
   "label": "Ambient",
   "count": 2
  },
  {
   "id": "productivity",
   "label": "Productivity",
   "count": 13
  },
  {
   "id": "device",
   "label": "Device",
   "count": 2
  }
 ],
 "apps": [
  {
   "id": "ambient",
   "name": "Ambient",
   "category": "ambient",
   "icon": "sparkles",
   "description": "Plasma, aurora, lava, zen, embers, fire, rain, stars, ripples, fireworks, snow, Life.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/ambient.html",
   "preview": "media/apps/ambient.gif",
   "poster": "media/apps/ambient.png",
   "og_image": "media/apps/og/ambient.png",
   "source": "src/deskdot/apps/ambient.py",
   "actions": [],
   "settings": [
    {
     "key": "effect",
     "title": "Effect",
     "type": "choice",
     "default": "plasma",
     "options": [
      {
       "value": "plasma",
       "label": "Plasma"
      },
      {
       "value": "aurora",
       "label": "Aurora"
      },
      {
       "value": "lava",
       "label": "Lava lamp"
      },
      {
       "value": "zen",
       "label": "Zen ripples"
      },
      {
       "value": "embers",
       "label": "Campfire embers"
      },
      {
       "value": "fire",
       "label": "Fire"
      },
      {
       "value": "matrix",
       "label": "Digital rain"
      },
      {
       "value": "starfield",
       "label": "Starfield"
      },
      {
       "value": "ripple",
       "label": "Ripple"
      },
      {
       "value": "life",
       "label": "Game of Life"
      },
      {
       "value": "fireworks",
       "label": "Fireworks"
      },
      {
       "value": "snow",
       "label": "Snowfall"
      },
      {
       "value": "tunnel",
       "label": "Tunnel"
      },
      {
       "value": "bounce",
       "label": "Bounce"
      }
     ]
    },
    {
     "key": "palette",
     "title": "Palette",
     "type": "choice",
     "default": "rainbow",
     "options": [
      {
       "value": "rainbow",
       "label": "Rainbow"
      },
      {
       "value": "ocean",
       "label": "Ocean"
      },
      {
       "value": "sunset",
       "label": "Sunset"
      },
      {
       "value": "forest",
       "label": "Forest"
      },
      {
       "value": "ember",
       "label": "Ember"
      },
      {
       "value": "neon",
       "label": "Neon"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Speed",
     "type": "number",
     "default": 0.8,
     "min": 0.2,
     "max": 3.0
    },
    {
     "key": "brightness",
     "title": "Intensity",
     "type": "number",
     "default": 0.85,
     "min": 0.1,
     "max": 1.0
    }
   ]
  },
  {
   "id": "loops",
   "name": "Loops",
   "category": "ambient",
   "icon": "infinity",
   "description": "Novelty loops: pixel cat on a rainbow trail, glyph rain, warp speed, Game of Life, bouncing logo.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/loops.html",
   "preview": "media/apps/loops.gif",
   "poster": "media/apps/loops.png",
   "og_image": "media/apps/og/loops.png",
   "source": "src/deskdot/apps/loops.py",
   "actions": [],
   "settings": [
    {
     "key": "effect",
     "title": "Loop",
     "type": "choice",
     "default": "cat",
     "options": [
      {
       "value": "cat",
       "label": "Pixel cat"
      },
      {
       "value": "rain",
       "label": "Glyph rain"
      },
      {
       "value": "warp",
       "label": "Warp speed"
      },
      {
       "value": "life",
       "label": "Game of Life"
      },
      {
       "value": "dvd",
       "label": "Bouncing logo"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "tint",
     "title": "Rain colour",
     "type": "color",
     "default": "#00ff5a"
    },
    {
     "key": "warp_color",
     "title": "Warp colour",
     "type": "color",
     "default": "#3c8cff"
    },
    {
     "key": "brightness",
     "title": "Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "casino_sevens",
   "name": "7 Up 7 Down",
   "category": "casino",
   "icon": "dices",
   "description": "The dice party game: bet under 7, over 7 or lucky 7 from your phone; the panel rolls.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_sevens.html",
   "preview": "media/apps/casino_sevens.gif",
   "poster": "media/apps/casino_sevens.png",
   "og_image": "media/apps/og/casino_sevens.png",
   "source": "src/deskdot/apps/casino_sevens.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "seven_pays",
     "title": "Lucky 7 pays (to 1)",
     "type": "integer",
     "description": "4:1 is the classic table (16.7 % edge on every bet); 5:1 makes the 7 a fair bet.",
     "default": 4,
     "min": 4,
     "max": 5
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_andarbahar",
   "name": "Andar Bahar",
   "category": "casino",
   "icon": "spade",
   "description": "The Indian card game: the joker is turned, bet Andar or Bahar, watch the cards fall.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_andarbahar.html",
   "preview": "media/apps/casino_andarbahar.gif",
   "poster": "media/apps/casino_andarbahar.png",
   "og_image": "media/apps/og/casino_andarbahar.png",
   "source": "src/deskdot/apps/casino_andarbahar.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "first",
     "title": "First card goes to",
     "type": "choice",
     "description": "The side that gets the first card wins 51.5 % of games, so it pays 0.9:1 (edge 2.15 %); the other side pays 1:1 (edge 3.00 %).",
     "default": "andar",
     "options": [
      {
       "value": "andar",
       "label": "Andar (the standard table)"
      },
      {
       "value": "bahar",
       "label": "Bahar"
      }
     ]
    },
    {
     "key": "side_bets",
     "title": "Card-count side bets",
     "type": "toggle",
     "description": "Bet on how many cards are dealt (1-5, 6-10 … 41-49).",
     "default": true
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_baccarat",
   "name": "Baccarat",
   "category": "casino",
   "icon": "spade",
   "description": "Punto Banco for parties: bet Player, Banker or Tie from your phone; the panel deals the coup.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_baccarat.html",
   "preview": "media/apps/casino_baccarat.gif",
   "poster": "media/apps/casino_baccarat.png",
   "og_image": "media/apps/og/casino_baccarat.png",
   "source": "src/deskdot/apps/casino_baccarat.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "commission",
     "title": "Banker commission",
     "type": "choice",
     "description": "Standard: Banker edge 1.06 %. No commission: a Banker win with 6 pays half (edge 1.46 %).",
     "default": "standard",
     "options": [
      {
       "value": "standard",
       "label": "Banker pays 0.95:1 (5 % commission)"
      },
      {
       "value": "no_commission",
       "label": "No commission (Banker 6 pays 1:2)"
      }
     ]
    },
    {
     "key": "tie_pays",
     "title": "Tie pays (to 1)",
     "type": "integer",
     "description": "8:1 = 14.4 % house edge; 9:1 = 4.8 %.",
     "default": 8,
     "min": 8,
     "max": 9
    },
    {
     "key": "decks",
     "title": "Decks",
     "type": "integer",
     "description": "A fresh shoe of this many decks every coup.",
     "default": 8,
     "min": 1,
     "max": 8
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_bigsix",
   "name": "Big Six",
   "category": "casino",
   "icon": "disc",
   "description": "The money wheel: bet on 1, 2, 5, 10, 20, joker or logo; the panel spins the big wheel.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_bigsix.html",
   "preview": "media/apps/casino_bigsix.gif",
   "poster": "media/apps/casino_bigsix.png",
   "og_image": "media/apps/og/casino_bigsix.png",
   "source": "src/deskdot/apps/casino_bigsix.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_blackjack",
   "name": "Blackjack",
   "category": "casino",
   "icon": "club",
   "description": "Blackjack for parties: everyone plays the dealer from their phone; the panel deals the hands.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_blackjack.html",
   "preview": "media/apps/casino_blackjack.gif",
   "poster": "media/apps/casino_blackjack.png",
   "og_image": "media/apps/og/casino_blackjack.png",
   "source": "src/deskdot/apps/casino_blackjack.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "decks",
     "title": "Decks in the shoe",
     "type": "integer",
     "default": 6,
     "min": 1,
     "max": 8
    },
    {
     "key": "penetration",
     "title": "Cut card at (% of the shoe)",
     "type": "integer",
     "description": "A new shoe starts after the cut card.",
     "default": 75,
     "min": 50,
     "max": 90
    },
    {
     "key": "soft17",
     "title": "Dealer on soft 17",
     "type": "choice",
     "default": "stand",
     "options": [
      {
       "value": "stand",
       "label": "Dealer stands on soft 17 (S17)"
      },
      {
       "value": "hit",
       "label": "Dealer hits soft 17 (H17, +0.22 %)"
      }
     ]
    },
    {
     "key": "blackjack_pays",
     "title": "Blackjack pays",
     "type": "choice",
     "default": "3:2",
     "options": [
      {
       "value": "3:2",
       "label": "Blackjack pays 3:2"
      },
      {
       "value": "6:5",
       "label": "Blackjack pays 6:5 (+1.4 % edge)"
      }
     ]
    },
    {
     "key": "double_on",
     "title": "Double on",
     "type": "choice",
     "default": "any",
     "options": [
      {
       "value": "any",
       "label": "Any first two cards"
      },
      {
       "value": "9-11",
       "label": "Hard 9, 10, 11 only"
      }
     ]
    },
    {
     "key": "double_after_split",
     "title": "Double after split",
     "type": "toggle",
     "default": true
    },
    {
     "key": "max_splits",
     "title": "Splits per hand",
     "type": "integer",
     "description": "3 = up to four hands.",
     "default": 3,
     "min": 1,
     "max": 3
    },
    {
     "key": "surrender",
     "title": "Surrender",
     "type": "choice",
     "default": "late",
     "options": [
      {
       "value": "late",
       "label": "Late surrender"
      },
      {
       "value": "none",
       "label": "No surrender"
      }
     ]
    },
    {
     "key": "insurance",
     "title": "Offer insurance (pays 2:1)",
     "type": "toggle",
     "default": true
    },
    {
     "key": "dealer_peek",
     "title": "Dealer peeks for blackjack",
     "type": "toggle",
     "description": "Off = European no-hole-card: a dealer blackjack also takes doubles and splits.",
     "default": true
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_roulette",
   "name": "Roulette",
   "category": "casino",
   "icon": "circle-dot",
   "description": "Casino roulette for parties: friends bet from their phones, the panel spins the wheel.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_roulette.html",
   "preview": "media/apps/casino_roulette.gif",
   "poster": "media/apps/casino_roulette.png",
   "og_image": "media/apps/og/casino_roulette.png",
   "source": "src/deskdot/apps/casino_roulette.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "wheel",
     "title": "Wheel",
     "type": "choice",
     "description": "European pays the same with one zero: 2.70 % house edge. American adds 00: 5.26 %.",
     "default": "european",
     "options": [
      {
       "value": "european",
       "label": "European (single 0)"
      },
      {
       "value": "american",
       "label": "American (0 and 00)"
      }
     ]
    },
    {
     "key": "la_partage",
     "title": "La Partage",
     "type": "toggle",
     "description": "European only: even-money bets get half back when the ball lands on 0 (edge 1.35 %).",
     "default": false
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_slots",
   "name": "Slots",
   "category": "casino",
   "icon": "cherry",
   "description": "A slot machine for every phone: pull the lever, watch your reels spin on the panel.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_slots.html",
   "preview": "media/apps/casino_slots.gif",
   "poster": "media/apps/casino_slots.png",
   "og_image": "media/apps/og/casino_slots.png",
   "source": "src/deskdot/apps/casino_slots.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others are a self-playing demo or one frozen phase, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo loop"
      },
      {
       "value": "betting",
       "label": "Preview: betting"
      },
      {
       "value": "locked",
       "label": "Preview: no more bets"
      },
      {
       "value": "spinning",
       "label": "Preview: spin / roll"
      },
      {
       "value": "result",
       "label": "Preview: result"
      },
      {
       "value": "board",
       "label": "Preview: result board"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "theme",
     "title": "Machine theme",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic fruits"
      },
      {
       "value": "neon",
       "label": "Neon 7s"
      },
      {
       "value": "space",
       "label": "Space"
      }
     ]
    },
    {
     "key": "volatility",
     "title": "Volatility",
     "type": "choice",
     "description": "Picks the reel-strip set. Every preset's RTP (≈95–96 %) is computed exactly and shown.",
     "default": "medium",
     "options": [
      {
       "value": "low",
       "label": "Low: frequent small wins"
      },
      {
       "value": "medium",
       "label": "Medium"
      },
      {
       "value": "high",
       "label": "High: rarer, bigger wins"
      }
     ]
    },
    {
     "key": "lines",
     "title": "Paylines",
     "type": "choice",
     "default": "5",
     "options": [
      {
       "value": "1",
       "label": "1 line (middle row)"
      },
      {
       "value": "3",
       "label": "3 lines (the rows)"
      },
      {
       "value": "5",
       "label": "5 lines (rows + diagonals)"
      }
     ]
    },
    {
     "key": "bet_per_line",
     "title": "Default bet per line",
     "type": "choice",
     "description": "Players pick their own bet per line on the phone; this is where it starts.",
     "default": "1",
     "options": [
      {
       "value": "1",
       "label": "1"
      },
      {
       "value": "2",
       "label": "2"
      },
      {
       "value": "5",
       "label": "5"
      },
      {
       "value": "10",
       "label": "10"
      },
      {
       "value": "25",
       "label": "25"
      }
     ]
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": [
     {
      "value": "classic",
      "label": "Classic fruits"
     },
     {
      "value": "neon",
      "label": "Neon 7s"
     },
     {
      "value": "space",
      "label": "Space"
     }
    ]
   }
  },
  {
   "id": "casino_teenpatti",
   "name": "Teen Patti",
   "category": "casino",
   "icon": "spade",
   "description": "Three-card Indian poker: play blind or seen, chaal, raise, sideshow and show from your phone.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_teenpatti.html",
   "preview": "media/apps/casino_teenpatti.gif",
   "poster": "media/apps/casino_teenpatti.png",
   "og_image": "media/apps/og/casino_teenpatti.png",
   "source": "src/deskdot/apps/casino_teenpatti.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others replay a demo hand played by bots, or freeze one moment of it, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo hand (bots)"
      },
      {
       "value": "betting",
       "label": "Preview: next hand"
      },
      {
       "value": "locked",
       "label": "Preview: shuffle"
      },
      {
       "value": "dealing",
       "label": "Preview: dealing"
      },
      {
       "value": "action",
       "label": "Preview: a turn"
      },
      {
       "value": "result",
       "label": "Preview: showdown"
      },
      {
       "value": "board",
       "label": "Preview: winner"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "boot",
     "title": "Boot (ante)",
     "type": "integer",
     "description": "Everyone's entry into each pot.",
     "default": 10,
     "min": 1,
     "max": 100000
    },
    {
     "key": "chaal_limit",
     "title": "Chaal limit",
     "type": "integer",
     "description": "The largest the stake may grow to.",
     "default": 1280,
     "min": 1,
     "max": 10000000
    },
    {
     "key": "pot_limit",
     "title": "Pot limit",
     "type": "integer",
     "description": "When the pot reaches it, everyone shows.",
     "default": 10240,
     "min": 2,
     "max": 100000000
    },
    {
     "key": "max_blind_rounds",
     "title": "Blind bets before you must see",
     "type": "integer",
     "description": "Then your cards open.",
     "default": 4,
     "min": 1,
     "max": 20
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "casino_holdem",
   "name": "Texas Hold'em",
   "category": "casino",
   "icon": "spade",
   "description": "No-limit poker for the party: phones hold the cards, the panel shows the board, pot and turns.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/casino_holdem.html",
   "preview": "media/apps/casino_holdem.gif",
   "poster": "media/apps/casino_holdem.png",
   "og_image": "media/apps/og/casino_holdem.png",
   "source": "src/deskdot/apps/casino_holdem.py",
   "actions": [],
   "settings": [
    {
     "key": "view",
     "title": "Panel view",
     "type": "choice",
     "description": "Live plays the real table. The others replay a demo hand played by bots, or freeze one moment of it, to check the art (no credits move).",
     "default": "live",
     "options": [
      {
       "value": "live",
       "label": "Live table"
      },
      {
       "value": "demo",
       "label": "Demo hand (bots)"
      },
      {
       "value": "betting",
       "label": "Preview: next hand"
      },
      {
       "value": "locked",
       "label": "Preview: shuffle"
      },
      {
       "value": "dealing",
       "label": "Preview: dealing"
      },
      {
       "value": "action",
       "label": "Preview: a turn"
      },
      {
       "value": "result",
       "label": "Preview: showdown"
      },
      {
       "value": "board",
       "label": "Preview: winner"
      },
      {
       "value": "lobby",
       "label": "Preview: join QR"
      },
      {
       "value": "paused",
       "label": "Preview: paused"
      }
     ],
     "group": "Preview"
    },
    {
     "key": "table_theme",
     "title": "Table theme",
     "type": "choice",
     "description": "The table's colours on the panel, every phone and the studio. Card suits, roulette pockets and players' colours never change.",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic green"
      },
      {
       "value": "royal",
       "label": "Royal blue"
      },
      {
       "value": "crimson",
       "label": "Crimson velvet"
      },
      {
       "value": "midnight",
       "label": "Midnight neon"
      },
      {
       "value": "strip",
       "label": "Neon strip"
      },
      {
       "value": "emerald",
       "label": "Emerald & champagne"
      },
      {
       "value": "burgundy",
       "label": "Burgundy & ivory"
      }
     ],
     "group": "Look"
    },
    {
     "key": "small_blind",
     "title": "Small blind",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 100000
    },
    {
     "key": "big_blind",
     "title": "Big blind",
     "type": "integer",
     "default": 10,
     "min": 2,
     "max": 200000
    },
    {
     "key": "blind_up_every",
     "title": "Double the blinds every N hands",
     "type": "integer",
     "description": "0 keeps the blinds fixed.",
     "default": 0,
     "min": 0,
     "max": 100
    },
    {
     "key": "rake_percent",
     "title": "Rake (%)",
     "type": "integer",
     "description": "The house's share of each pot at a showdown. 0 = none.",
     "default": 0,
     "min": 0,
     "max": 10
    },
    {
     "key": "rake_cap",
     "title": "Rake cap per hand",
     "type": "integer",
     "description": "0 = no cap.",
     "default": 0,
     "min": 0,
     "max": 1000000
    }
   ],
   "game": {
    "max_players": 9,
    "controls": [
     "casino"
    ],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "wireframe",
   "name": "3D Wireframe Vector",
   "category": "creative",
   "icon": "square",
   "description": "Retro 80s 3D vector graphics and 4D tesseract holo-display with depth cueing.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/wireframe.html",
   "preview": "media/apps/wireframe.gif",
   "poster": "media/apps/wireframe.png",
   "og_image": "media/apps/og/wireframe.png",
   "source": "src/deskdot/apps/wireframe.py",
   "actions": [],
   "settings": [
    {
     "key": "shape",
     "title": "Geometric Shape",
     "type": "choice",
     "default": "tesseract",
     "options": [
      {
       "value": "tesseract",
       "label": "4D Hypercube Tesseract"
      },
      {
       "value": "icosahedron",
       "label": "3D Geodesic Icosahedron"
      },
      {
       "value": "torus",
       "label": "3D Wireframe Torus"
      },
      {
       "value": "diamond",
       "label": "Faceted Diamond Gem"
      },
      {
       "value": "starfighter",
       "label": "Vector Starfighter"
      }
     ]
    },
    {
     "key": "palette",
     "title": "Holo Palette",
     "type": "choice",
     "default": "cyber_cyan",
     "options": [
      {
       "value": "cyber_cyan",
       "label": "Electric Cyan"
      },
      {
       "value": "laser_green",
       "label": "Laser Vector Green"
      },
      {
       "value": "neon_magenta",
       "label": "Neon Magenta"
      },
      {
       "value": "solar_gold",
       "label": "Solar Amber Gold"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Rotation Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "canvas",
   "name": "Canvas",
   "category": "creative",
   "icon": "brush",
   "description": "Pixel-paint straight onto the LEDs. Your drawing is saved.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/canvas.html",
   "preview": "media/apps/canvas.gif",
   "poster": "media/apps/canvas.png",
   "og_image": "media/apps/og/canvas.png",
   "source": "src/deskdot/apps/canvas.py",
   "actions": [
    {
     "id": "clear",
     "label": "Clear"
    }
   ],
   "settings": []
  },
  {
   "id": "emotes",
   "name": "Emotes",
   "category": "creative",
   "icon": "face-slightly-smiling-plus",
   "description": "Full-screen animated emotes: laughing King, crying, GG, fire, 100, rage quit…",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/emotes.html",
   "preview": "media/apps/emotes.gif",
   "poster": "media/apps/emotes.png",
   "og_image": "media/apps/og/emotes.png",
   "source": "src/deskdot/apps/emotes.py",
   "actions": [],
   "settings": [
    {
     "key": "emote",
     "title": "Emote",
     "type": "choice",
     "default": "laugh",
     "options": [
      {
       "value": "laugh",
       "label": "Laughing"
      },
      {
       "value": "cry",
       "label": "Crying"
      },
      {
       "value": "angry",
       "label": "Angry"
      },
      {
       "value": "thumbs",
       "label": "Thumbs up"
      },
      {
       "value": "love",
       "label": "Heart eyes"
      },
      {
       "value": "wow",
       "label": "Wow"
      },
      {
       "value": "cool",
       "label": "Cool"
      },
      {
       "value": "wink",
       "label": "Wink"
      },
      {
       "value": "sleepy",
       "label": "Sleepy"
      },
      {
       "value": "think",
       "label": "Thinking"
      },
      {
       "value": "skull",
       "label": "Skull"
      },
      {
       "value": "fire",
       "label": "On fire"
      },
      {
       "value": "gg",
       "label": "GG"
      },
      {
       "value": "party",
       "label": "Party"
      },
      {
       "value": "clap",
       "label": "Clap"
      },
      {
       "value": "hundred",
       "label": "100"
      },
      {
       "value": "rage",
       "label": "Rage quit"
      }
     ]
    },
    {
     "key": "style",
     "title": "Face",
     "type": "choice",
     "default": "smiley",
     "options": [
      {
       "value": "smiley",
       "label": "Smiley"
      },
      {
       "value": "king",
       "label": "Arena King"
      },
      {
       "value": "goblin",
       "label": "Goblin"
      },
      {
       "value": "cat",
       "label": "Cat"
      }
     ]
    },
    {
     "key": "cycle",
     "title": "Cycle through all emotes",
     "type": "toggle",
     "default": false
    },
    {
     "key": "background",
     "title": "Background",
     "type": "choice",
     "default": "rays",
     "options": [
      {
       "value": "none",
       "label": "Black"
      },
      {
       "value": "rays",
       "label": "Sun rays"
      },
      {
       "value": "burst",
       "label": "Burst"
      },
      {
       "value": "hearts",
       "label": "Hearts"
      }
     ]
    },
    {
     "key": "skin",
     "title": "Skin",
     "type": "color",
     "description": "Overrides the face colour",
     "default": "#ffc800"
    },
    {
     "key": "custom_skin",
     "title": "Use custom skin",
     "type": "toggle",
     "default": false
    },
    {
     "key": "speed",
     "title": "Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.25,
     "max": 3.0
    }
   ]
  },
  {
   "id": "sand",
   "name": "Falling Sand",
   "category": "creative",
   "icon": "square",
   "description": "Powder Toy cellular automaton with sand, water, fire, lava, plants, and acid.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/sand.html",
   "preview": "media/apps/sand.gif",
   "poster": "media/apps/sand.png",
   "og_image": "media/apps/og/sand.png",
   "source": "src/deskdot/apps/sand.py",
   "actions": [],
   "settings": [
    {
     "key": "scenario",
     "title": "Simulation Scenario",
     "type": "choice",
     "default": "hourglass",
     "options": [
      {
       "value": "hourglass",
       "label": "Hourglass"
      },
      {
       "value": "volcano",
       "label": "Lava Volcano"
      },
      {
       "value": "oasis",
       "label": "Waterfall Oasis"
      },
      {
       "value": "acid_lab",
       "label": "Acid Laboratory"
      },
      {
       "value": "elemental",
       "label": "Elemental Crucible"
      }
     ]
    },
    {
     "key": "palette",
     "title": "Color Palette",
     "type": "choice",
     "default": "vibrant",
     "options": [
      {
       "value": "vibrant",
       "label": "Vibrant"
      },
      {
       "value": "retro",
       "label": "Retro 8-Bit"
      },
      {
       "value": "neon",
       "label": "Cyber Neon"
      },
      {
       "value": "pastel",
       "label": "Soft Pastel"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Simulation Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "boids",
   "name": "Flocking Boids",
   "category": "creative",
   "icon": "square",
   "description": "Craig Reynolds flocking swarm simulation and bioluminescent particle life.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/boids.html",
   "preview": "media/apps/boids.gif",
   "poster": "media/apps/boids.png",
   "og_image": "media/apps/og/boids.png",
   "source": "src/deskdot/apps/boids.py",
   "actions": [],
   "settings": [
    {
     "key": "theme",
     "title": "Visual Theme",
     "type": "choice",
     "default": "deep_sea",
     "options": [
      {
       "value": "deep_sea",
       "label": "Bioluminescent Abyss"
      },
      {
       "value": "starlings",
       "label": "Twilight Murmuration"
      },
      {
       "value": "fireflies",
       "label": "Summer Fireflies"
      },
      {
       "value": "cyber",
       "label": "Cyberpunk Nanobots"
      }
     ]
    },
    {
     "key": "predator",
     "title": "Predator Mode",
     "type": "choice",
     "default": "off",
     "options": [
      {
       "value": "off",
       "label": "Peaceful Flock"
      },
      {
       "value": "on",
       "label": "Apex Predator Chasing"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Swarm Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "flybrain",
   "name": "Fly Brain",
   "category": "creative",
   "icon": "bug",
   "description": "A fruit fly driven by a model of its real visual circuit (FlyWire connectome): it hunts fruit, dodges a looming swatter. Its neurons firing and the keys it presses play in the studio's 3D Fly view.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/flybrain.html",
   "preview": "media/apps/flybrain.gif",
   "poster": "media/apps/flybrain.png",
   "og_image": "media/apps/og/flybrain.png",
   "source": "src/deskdot/apps/flybrain.py",
   "actions": [],
   "settings": [
    {
     "key": "danger",
     "title": "Swatter",
     "type": "choice",
     "default": "normal",
     "options": [
      {
       "value": "calm",
       "label": "Calm (no swatter)"
      },
      {
       "value": "normal",
       "label": "Normal"
      },
      {
       "value": "hectic",
       "label": "Hectic"
      }
     ]
    },
    {
     "key": "fruit",
     "title": "Fruit in the arena",
     "type": "integer",
     "default": 2,
     "min": 1,
     "max": 3
    },
    {
     "key": "seed",
     "title": "Fly",
     "type": "integer",
     "description": "Each number is a different fly and arena",
     "default": 7,
     "min": 0,
     "max": 9999
    }
   ]
  },
  {
   "id": "fontlab",
   "name": "Font Lab",
   "category": "creative",
   "icon": "case-sensitive",
   "description": "Every font (built-in and pixel TTFs like Minecraft / VCR OSD), styles, fills and text animations.",
   "kind": "stream",
   "kinds": [
    "clip",
    "stream"
   ],
   "page": "apps/fontlab.html",
   "preview": "media/apps/fontlab.gif",
   "poster": "media/apps/fontlab.png",
   "og_image": "media/apps/og/fontlab.png",
   "source": "src/deskdot/apps/fontlab.py",
   "actions": [],
   "settings": [
    {
     "key": "text",
     "title": "Text",
     "type": "text",
     "default": "PIXEL",
     "max_length": 60
    },
    {
     "key": "font",
     "title": "Font",
     "type": "choice",
     "default": "small",
     "options": [
      {
       "value": "tiny",
       "label": "Built-in tiny (5px)"
      },
      {
       "value": "small",
       "label": "Built-in small (7px)"
      },
      {
       "value": "big",
       "label": "Built-in big digits (10px)"
      },
      {
       "value": "ttf:Minecraft.ttf",
       "label": "Minecraft"
      },
      {
       "value": "ttf:VCR_OSD_MONO_1.001.ttf",
       "label": "VCR OSD MONO 1.001"
      }
     ]
    },
    {
     "key": "size",
     "title": "TTF size (px)",
     "type": "integer",
     "default": 12,
     "min": 6,
     "max": 32
    },
    {
     "key": "spacing",
     "title": "Letter spacing",
     "type": "integer",
     "default": 1,
     "min": -1,
     "max": 6
    },
    {
     "key": "uppercase",
     "title": "Uppercase",
     "type": "toggle",
     "default": true
    },
    {
     "key": "style",
     "title": "Style",
     "type": "choice",
     "default": "plain",
     "options": [
      {
       "value": "plain",
       "label": "Plain"
      },
      {
       "value": "bold",
       "label": "Bold"
      },
      {
       "value": "outline",
       "label": "Outline"
      },
      {
       "value": "shadow",
       "label": "Shadow"
      },
      {
       "value": "italic",
       "label": "Italic"
      },
      {
       "value": "double",
       "label": "Double (2x)"
      },
      {
       "value": "neon",
       "label": "Neon glow"
      }
     ]
    },
    {
     "key": "align",
     "title": "Align",
     "type": "choice",
     "default": "center",
     "options": [
      {
       "value": "left",
       "label": "Left"
      },
      {
       "value": "center",
       "label": "Centre"
      },
      {
       "value": "right",
       "label": "Right"
      }
     ]
    },
    {
     "key": "valign",
     "title": "Vertical",
     "type": "choice",
     "default": "middle",
     "options": [
      {
       "value": "top",
       "label": "Top"
      },
      {
       "value": "middle",
       "label": "Middle"
      },
      {
       "value": "bottom",
       "label": "Bottom"
      }
     ]
    },
    {
     "key": "color",
     "title": "Colour",
     "type": "color",
     "default": "#ffffff"
    },
    {
     "key": "color2",
     "title": "Second colour",
     "type": "color",
     "default": "#ff00be"
    },
    {
     "key": "fill",
     "title": "Fill",
     "type": "choice",
     "default": "solid",
     "options": [
      {
       "value": "solid",
       "label": "Solid"
      },
      {
       "value": "gradient",
       "label": "Gradient"
      },
      {
       "value": "rainbow",
       "label": "Rainbow"
      },
      {
       "value": "stripes",
       "label": "Stripes"
      },
      {
       "value": "fire",
       "label": "Fire"
      }
     ]
    },
    {
     "key": "animation",
     "title": "Animation",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "scroll",
       "label": "Scroll"
      },
      {
       "value": "typewriter",
       "label": "Typewriter"
      },
      {
       "value": "wave",
       "label": "Wave"
      },
      {
       "value": "bounce",
       "label": "Bounce"
      },
      {
       "value": "flicker",
       "label": "Flicker"
      },
      {
       "value": "zoom",
       "label": "Pulse"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 4.0
    },
    {
     "key": "background",
     "title": "Background",
     "type": "color",
     "default": "#000000"
    }
   ]
  },
  {
   "id": "brain",
   "name": "Neuromorphic Cortex",
   "category": "creative",
   "icon": "square",
   "description": "1,024-neuron biological spiking neural network simulation with traveling waves.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/brain.html",
   "preview": "media/apps/brain.gif",
   "poster": "media/apps/brain.png",
   "og_image": "media/apps/og/brain.png",
   "source": "src/deskdot/apps/brain.py",
   "actions": [],
   "settings": [
    {
     "key": "mode",
     "title": "Neural Dynamics Mode",
     "type": "choice",
     "default": "cortex",
     "options": [
      {
       "value": "cortex",
       "label": "Resting Cortex Waves"
      },
      {
       "value": "spiral",
       "label": "Cortical Spiral Vortex"
      },
      {
       "value": "burst",
       "label": "Synchronized Gamma Burst"
      },
      {
       "value": "synapse",
       "label": "Axonal Signal Cascade"
      }
     ]
    },
    {
     "key": "palette",
     "title": "Spike Palette",
     "type": "choice",
     "default": "bioluminescent",
     "options": [
      {
       "value": "bioluminescent",
       "label": "Bioluminescent Cyan"
      },
      {
       "value": "neuro_gold",
       "label": "Synaptic Gold"
      },
      {
       "value": "infrared",
       "label": "Infrared Thermal"
      },
      {
       "value": "matrix",
       "label": "Neural Matrix"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Oscillation Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "pixabots",
   "name": "Pixabots",
   "category": "creative",
   "icon": "bot",
   "description": "Animated 32x32 pixel art robot avatars with idle bounce, blinking, and accessories.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/pixabots.html",
   "preview": "media/apps/pixabots.gif",
   "poster": "media/apps/pixabots.png",
   "og_image": "media/apps/og/pixabots.png",
   "source": "src/deskdot/apps/pixabots.py",
   "actions": [],
   "settings": [
    {
     "key": "character",
     "title": "Character",
     "type": "choice",
     "default": "sparky",
     "options": [
      {
       "value": "sparky",
       "label": "Sparky (Retro Bot)"
      },
      {
       "value": "visor",
       "label": "Visor (Cyberpunk)"
      },
      {
       "value": "crt",
       "label": "CRT Monitor (Smiley)"
      },
      {
       "value": "nyan",
       "label": "Nyan Cat (Cosmic)"
      },
      {
       "value": "parrot",
       "label": "Party Parrot (Dance)"
      },
      {
       "value": "astro",
       "label": "Astro Bot (Explorer)"
      }
     ]
    },
    {
     "key": "color_scheme",
     "title": "Color Palette",
     "type": "choice",
     "default": "default",
     "options": [
      {
       "value": "default",
       "label": "Default"
      },
      {
       "value": "cyberpunk",
       "label": "Cyberpunk"
      },
      {
       "value": "pico8",
       "label": "Pico8"
      },
      {
       "value": "gameboy",
       "label": "Gameboy"
      },
      {
       "value": "neon",
       "label": "Neon"
      }
     ]
    },
    {
     "key": "accessory",
     "title": "Accessory",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "antenna",
       "label": "Beacon Antenna"
      },
      {
       "value": "headphones",
       "label": "DJ Headphones"
      },
      {
       "value": "crown",
       "label": "Royal Crown"
      },
      {
       "value": "party_hat",
       "label": "Party Hat"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "sparkles",
     "title": "Sparkles & Effects",
     "type": "toggle",
     "default": true
    }
   ]
  },
  {
   "id": "avatar",
   "name": "Pixel Avatar",
   "category": "creative",
   "icon": "user-round",
   "description": "Minecraft faces, GitHub identicons and DiceBear pixel characters — pixel-exact, with a name tag.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/avatar.html",
   "preview": "media/apps/avatar.gif",
   "poster": "media/apps/avatar.png",
   "og_image": "media/apps/og/avatar.png",
   "source": "src/deskdot/apps/avatar.py",
   "actions": [],
   "settings": [
    {
     "key": "names",
     "title": "Names",
     "type": "text",
     "description": "Comma-separated. Prefix gh: for a GitHub identicon, db: for a DiceBear character, mc: for a Minecraft face; unprefixed names use the default source",
     "default": "Notch, jeb_, gh:torvalds, db:deskdot",
     "max_length": 400
    },
    {
     "key": "source",
     "title": "Default source",
     "type": "choice",
     "default": "minecraft",
     "options": [
      {
       "value": "minecraft",
       "label": "Minecraft face"
      },
      {
       "value": "github",
       "label": "GitHub identicon"
      },
      {
       "value": "dicebear",
       "label": "DiceBear pixel art"
      }
     ]
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "framed",
     "options": [
      {
       "value": "framed",
       "label": "Face + name tag"
      },
      {
       "value": "full",
       "label": "Full screen"
      }
     ]
    },
    {
     "key": "tag",
     "title": "Name tag",
     "type": "choice",
     "default": "intro",
     "options": [
      {
       "value": "off",
       "label": "Off"
      },
      {
       "value": "intro",
       "label": "Flash on change"
      },
      {
       "value": "always",
       "label": "Always"
      }
     ]
    },
    {
     "key": "animation",
     "title": "Animation",
     "type": "choice",
     "default": "both",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "bob",
       "label": "Bob"
      },
      {
       "value": "blink",
       "label": "Blink"
      },
      {
       "value": "both",
       "label": "Bob + blink"
      }
     ]
    },
    {
     "key": "seconds",
     "title": "Seconds per name",
     "type": "integer",
     "default": 8,
     "min": 2,
     "max": 120
    },
    {
     "key": "background",
     "title": "Background",
     "type": "choice",
     "default": "source",
     "options": [
      {
       "value": "black",
       "label": "Black"
      },
      {
       "value": "source",
       "label": "Source tint"
      },
      {
       "value": "grid",
       "label": "Grid"
      }
     ]
    }
   ]
  },
  {
   "id": "playercard",
   "name": "Player Card",
   "category": "creative",
   "icon": "id-card",
   "description": "Your own rated football card — tiers, portrait, nation, six stats, shine and flip.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/playercard.html",
   "preview": "media/apps/playercard.gif",
   "poster": "media/apps/playercard.png",
   "og_image": "media/apps/og/playercard.png",
   "source": "src/deskdot/apps/cards.py",
   "actions": [],
   "settings": [
    {
     "key": "name",
     "title": "Name",
     "type": "text",
     "default": "DESKDOT",
     "max_length": 14
    },
    {
     "key": "rating",
     "title": "Overall",
     "type": "integer",
     "default": 91,
     "min": 1,
     "max": 99
    },
    {
     "key": "position",
     "title": "Position",
     "type": "choice",
     "default": "ST",
     "options": [
      {
       "value": "GK",
       "label": "GK"
      },
      {
       "value": "CB",
       "label": "CB"
      },
      {
       "value": "LB",
       "label": "LB"
      },
      {
       "value": "RB",
       "label": "RB"
      },
      {
       "value": "CDM",
       "label": "CDM"
      },
      {
       "value": "CM",
       "label": "CM"
      },
      {
       "value": "CAM",
       "label": "CAM"
      },
      {
       "value": "LW",
       "label": "LW"
      },
      {
       "value": "RW",
       "label": "RW"
      },
      {
       "value": "CF",
       "label": "CF"
      },
      {
       "value": "ST",
       "label": "ST"
      }
     ]
    },
    {
     "key": "tier",
     "title": "Card",
     "type": "choice",
     "default": "gold",
     "options": [
      {
       "value": "gold",
       "label": "Gold"
      },
      {
       "value": "silver",
       "label": "Silver"
      },
      {
       "value": "bronze",
       "label": "Bronze"
      },
      {
       "value": "icon",
       "label": "Icon"
      },
      {
       "value": "totw",
       "label": "Team of the week"
      },
      {
       "value": "hero",
       "label": "Hero"
      },
      {
       "value": "special",
       "label": "Special"
      }
     ]
    },
    {
     "key": "pac",
     "title": "Pace",
     "type": "integer",
     "default": 92,
     "min": 1,
     "max": 99,
     "group": "Stats"
    },
    {
     "key": "sho",
     "title": "Shooting",
     "type": "integer",
     "default": 89,
     "min": 1,
     "max": 99,
     "group": "Stats"
    },
    {
     "key": "pas",
     "title": "Passing",
     "type": "integer",
     "default": 84,
     "min": 1,
     "max": 99,
     "group": "Stats"
    },
    {
     "key": "dri",
     "title": "Dribbling",
     "type": "integer",
     "default": 90,
     "min": 1,
     "max": 99,
     "group": "Stats"
    },
    {
     "key": "defending",
     "title": "Defending",
     "type": "integer",
     "default": 45,
     "min": 1,
     "max": 99,
     "group": "Stats"
    },
    {
     "key": "phy",
     "title": "Physical",
     "type": "integer",
     "default": 78,
     "min": 1,
     "max": 99,
     "group": "Stats"
    },
    {
     "key": "skin",
     "title": "Skin",
     "type": "color",
     "default": "#d9a066",
     "group": "Player look"
    },
    {
     "key": "hair",
     "title": "Hair colour",
     "type": "color",
     "default": "#2b1a0e",
     "group": "Player look"
    },
    {
     "key": "hair_style",
     "title": "Hair style",
     "type": "choice",
     "default": "short",
     "options": [
      {
       "value": "short",
       "label": "Short"
      },
      {
       "value": "buzz",
       "label": "Buzz"
      },
      {
       "value": "curly",
       "label": "Curly"
      },
      {
       "value": "long",
       "label": "Long"
      },
      {
       "value": "mohawk",
       "label": "Mohawk"
      },
      {
       "value": "bald",
       "label": "Bald"
      },
      {
       "value": "bun",
       "label": "Man bun"
      }
     ],
     "group": "Player look"
    },
    {
     "key": "kit",
     "title": "Kit",
     "type": "color",
     "default": "#e8e8f0",
     "group": "Player look"
    },
    {
     "key": "nation_1",
     "title": "Flag stripe 1",
     "type": "color",
     "default": "#ff9933",
     "group": "Nation"
    },
    {
     "key": "nation_2",
     "title": "Flag stripe 2",
     "type": "color",
     "default": "#ffffff",
     "group": "Nation"
    },
    {
     "key": "nation_3",
     "title": "Flag stripe 3",
     "type": "color",
     "default": "#138808",
     "group": "Nation"
    },
    {
     "key": "shine",
     "title": "Shine sweep",
     "type": "toggle",
     "default": true
    },
    {
     "key": "flip_to_stats",
     "title": "Flip to stats page",
     "type": "toggle",
     "default": true
    },
    {
     "key": "page_seconds",
     "title": "Seconds per side",
     "type": "integer",
     "default": 5,
     "min": 2,
     "max": 30
    }
   ]
  },
  {
   "id": "synthwave",
   "name": "Synthwave Horizon",
   "category": "creative",
   "icon": "square",
   "description": "80s cyberpunk outrun horizon with 3D perspective grid, blazing sun, and sports car.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/synthwave.html",
   "preview": "media/apps/synthwave.gif",
   "poster": "media/apps/synthwave.png",
   "og_image": "media/apps/og/synthwave.png",
   "source": "src/deskdot/apps/synthwave.py",
   "actions": [],
   "settings": [
    {
     "key": "theme",
     "title": "Visual Theme",
     "type": "choice",
     "default": "outrun",
     "options": [
      {
       "value": "outrun",
       "label": "Neon Outrun"
      },
      {
       "value": "miami",
       "label": "Miami Sunset"
      },
      {
       "value": "cyber_red",
       "label": "Blood Cyber"
      },
      {
       "value": "emerald",
       "label": "Matrix Emerald"
      }
     ]
    },
    {
     "key": "vehicle",
     "title": "Cruiser Vehicle",
     "type": "choice",
     "default": "car",
     "options": [
      {
       "value": "car",
       "label": "Retro Sports Car"
      },
      {
       "value": "ship",
       "label": "Hover Cruiser"
      },
      {
       "value": "none",
       "label": "Empty Highway"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Highway Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "text",
   "name": "Text",
   "category": "creative",
   "icon": "type",
   "description": "Messages, auto-fitted, smoothly scrolling, or flowing like Spotify lyrics.",
   "kind": "stream",
   "kinds": [
    "clip",
    "stream"
   ],
   "page": "apps/text.html",
   "preview": "media/apps/text.gif",
   "poster": "media/apps/text.png",
   "og_image": "media/apps/og/text.png",
   "source": "src/deskdot/apps/text.py",
   "actions": [],
   "settings": [
    {
     "key": "text",
     "title": "Text",
     "type": "text",
     "default": "HELLO WORLD",
     "max_length": 500
    },
    {
     "key": "font",
     "title": "Font",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto fit"
      },
      {
       "value": "small",
       "label": "Small (7px)"
      },
      {
       "value": "tiny",
       "label": "Tiny (5px)"
      }
     ]
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto (Lyrics)"
      },
      {
       "value": "lyrics",
       "label": "Spotify Lyrics (Vertical)"
      },
      {
       "value": "scroll",
       "label": "Marquee (Horizontal)"
      },
      {
       "value": "static",
       "label": "Static (Wrap)"
      }
     ]
    },
    {
     "key": "color",
     "title": "Colour",
     "type": "color",
     "default": "#ffffff"
    },
    {
     "key": "effect",
     "title": "Effect",
     "type": "choice",
     "default": "solid",
     "options": [
      {
       "value": "solid",
       "label": "Solid"
      },
      {
       "value": "rainbow",
       "label": "Rainbow"
      },
      {
       "value": "gradient",
       "label": "Gradient"
      }
     ]
    },
    {
     "key": "color2",
     "title": "Gradient end",
     "type": "color",
     "default": "#ff00be"
    },
    {
     "key": "frame",
     "title": "Frame",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "underline",
       "label": "Underline"
      },
      {
       "value": "border",
       "label": "Border"
      },
      {
       "value": "glow",
       "label": "Glow bar"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Scroll speed (px/s)",
     "type": "integer",
     "default": 18,
     "min": 4,
     "max": 40
    }
   ]
  },
  {
   "id": "composer",
   "name": "Text Studio",
   "category": "creative",
   "icon": "text-cursor-input",
   "description": "Click the preview to place text and live values — drag, restyle, animate. Your own layout.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/composer.html",
   "preview": "media/apps/composer.gif",
   "poster": "media/apps/composer.png",
   "og_image": "media/apps/og/composer.png",
   "source": "src/deskdot/apps/composer.py",
   "actions": [],
   "settings": [
    {
     "key": "layers",
     "title": "Layers",
     "type": "list of object"
    },
    {
     "key": "background",
     "title": "Background",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "Black"
      },
      {
       "value": "solid",
       "label": "Solid"
      },
      {
       "value": "gradient",
       "label": "Gradient"
      },
      {
       "value": "border",
       "label": "Border"
      },
      {
       "value": "stars",
       "label": "Twinkle"
      }
     ]
    },
    {
     "key": "bg",
     "title": "Background",
     "type": "color",
     "default": "#10061a"
    },
    {
     "key": "bg2",
     "title": "Background 2",
     "type": "color",
     "default": "#001428"
    }
   ]
  },
  {
   "id": "airquality",
   "name": "Air Quality",
   "category": "data",
   "icon": "wind",
   "description": "AQI gauge, PM2.5/PM10/O3/NO2 bars, UV index advice, pollen (Europe) and a 24 h forecast.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/airquality.html",
   "preview": "media/apps/airquality.gif",
   "poster": "media/apps/airquality.png",
   "og_image": "media/apps/og/airquality.png",
   "source": "src/deskdot/apps/airquality.py",
   "actions": [],
   "settings": [
    {
     "key": "index",
     "title": "Index",
     "type": "choice",
     "default": "us",
     "options": [
      {
       "value": "us",
       "label": "US AQI"
      },
      {
       "value": "eu",
       "label": "European AQI"
      }
     ],
     "group": "Data"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "gauge",
     "options": [
      {
       "value": "gauge",
       "label": "AQI gauge"
      },
      {
       "value": "pollutants",
       "label": "Pollutants"
      },
      {
       "value": "uv",
       "label": "UV index"
      },
      {
       "value": "forecast",
       "label": "24 h forecast"
      },
      {
       "value": "cycle",
       "label": "Cycle all"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "gauge",
     "title": "Gauge style",
     "type": "choice",
     "default": "arc",
     "options": [
      {
       "value": "arc",
       "label": "Arc"
      },
      {
       "value": "ring",
       "label": "Edge ring"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per page",
     "type": "integer",
     "default": 8,
     "min": 3,
     "max": 60,
     "group": "Layout"
    },
    {
     "key": "label_color",
     "title": "Label colour",
     "type": "color",
     "default": "#8c8ca0",
     "group": "Colours"
    },
    {
     "key": "alert",
     "title": "Alert when AQI rises past",
     "type": "toggle",
     "default": true,
     "group": "Alerts"
    },
    {
     "key": "alert_aqi",
     "title": "Alert threshold",
     "type": "integer",
     "description": "In the chosen index (US 150 = unhealthy, EU 80 = very poor)",
     "default": 150,
     "min": 20,
     "max": 500,
     "group": "Alerts"
    }
   ]
  },
  {
   "id": "markets",
   "name": "Crypto Ticker",
   "category": "data",
   "icon": "chart-candlestick",
   "description": "Binance prices with 24-hour change and trend line.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/markets.html",
   "preview": "media/apps/markets.gif",
   "poster": "media/apps/markets.png",
   "og_image": "media/apps/og/markets.png",
   "source": "src/deskdot/apps/markets.py",
   "actions": [],
   "settings": [
    {
     "key": "symbols",
     "title": "Symbols",
     "type": "text",
     "description": "Comma-separated, quoted in USDT",
     "default": "BTC,ETH,SOL"
    },
    {
     "key": "rotate",
     "title": "Seconds per symbol",
     "type": "integer",
     "default": 6,
     "min": 2,
     "max": 120
    },
    {
     "key": "currency",
     "title": "Currency sign",
     "type": "text",
     "default": "$",
     "max_length": 2
    }
   ]
  },
  {
   "id": "currency",
   "name": "Currency",
   "category": "data",
   "icon": "banknote",
   "description": "Exchange rates for your home currency: big pair with a sparkline, board, converter, heat grid.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/currency.html",
   "preview": "media/apps/currency.gif",
   "poster": "media/apps/currency.png",
   "og_image": "media/apps/og/currency.png",
   "source": "src/deskdot/apps/currency.py",
   "actions": [],
   "settings": [
    {
     "key": "base",
     "title": "Home currency",
     "type": "text",
     "description": "ISO code, e.g. INR, USD, EUR, AED",
     "default": "INR",
     "max_length": 6,
     "group": "Currencies"
    },
    {
     "key": "quotes",
     "title": "Currencies",
     "type": "text",
     "description": "Comma-separated: USD, EUR, GBP, JPY, AED, SGD, BTC… (fiat, metals and crypto)",
     "default": "USD,EUR,GBP",
     "group": "Currencies"
    },
    {
     "key": "direction",
     "title": "Show",
     "type": "choice",
     "default": "to_home",
     "options": [
      {
       "value": "to_home",
       "label": "1 USD = ₹95.7"
      },
      {
       "value": "from_home",
       "label": "1 INR = $0.0104"
      }
     ],
     "group": "Currencies"
    },
    {
     "key": "amount",
     "title": "Converter amount",
     "type": "number",
     "default": 100.0,
     "min": 0.01,
     "max": 1000000000.0,
     "group": "Currencies"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "pair",
     "options": [
      {
       "value": "pair",
       "label": "Big pair"
      },
      {
       "value": "board",
       "label": "Board"
      },
      {
       "value": "converter",
       "label": "Converter"
      },
      {
       "value": "heat",
       "label": "Heat grid"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "range",
     "title": "Chart range",
     "type": "choice",
     "default": "30d",
     "options": [
      {
       "value": "7d",
       "label": "7D"
      },
      {
       "value": "30d",
       "label": "30D"
      },
      {
       "value": "90d",
       "label": "90D"
      },
      {
       "value": "1y",
       "label": "1Y"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "change",
     "title": "Change",
     "type": "choice",
     "default": "day",
     "options": [
      {
       "value": "day",
       "label": "Since yesterday"
      },
      {
       "value": "range",
       "label": "Over the chart range"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per page",
     "type": "integer",
     "default": 8,
     "min": 2,
     "max": 120,
     "group": "Layout"
    },
    {
     "key": "up",
     "title": "Up colour",
     "type": "color",
     "default": "#00ff78",
     "group": "Colours"
    },
    {
     "key": "down",
     "title": "Down colour",
     "type": "color",
     "default": "#ff143c",
     "group": "Colours"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#ffaa00",
     "group": "Colours"
    },
    {
     "key": "home_view",
     "title": "Home view colours",
     "type": "toggle",
     "description": "Red when the home currency weakens (the foreign price rises)",
     "default": false,
     "group": "Colours"
    },
    {
     "key": "heat_scale",
     "title": "Full-scale change %",
     "type": "number",
     "default": 1.0,
     "min": 0.1,
     "max": 10.0,
     "group": "Colours"
    }
   ]
  },
  {
   "id": "quakes",
   "name": "Earthquakes",
   "category": "data",
   "icon": "activity",
   "description": "Live USGS earthquakes: world map with pulsing rings, regional zoom, nearest/latest card, list.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/quakes.html",
   "preview": "media/apps/quakes.gif",
   "poster": "media/apps/quakes.png",
   "og_image": "media/apps/og/quakes.png",
   "source": "src/deskdot/apps/quakes.py",
   "actions": [],
   "settings": [
    {
     "key": "feed",
     "title": "Feed",
     "type": "choice",
     "default": "2.5_day",
     "options": [
      {
       "value": "all_hour",
       "label": "All · past hour"
      },
      {
       "value": "all_day",
       "label": "All · past day"
      },
      {
       "value": "2.5_day",
       "label": "M2.5+ · past day"
      },
      {
       "value": "2.5_week",
       "label": "M2.5+ · past week"
      },
      {
       "value": "4.5_day",
       "label": "M4.5+ · past day"
      },
      {
       "value": "4.5_week",
       "label": "M4.5+ · past week"
      },
      {
       "value": "4.5_month",
       "label": "M4.5+ · past 30 days"
      },
      {
       "value": "significant_week",
       "label": "Significant · past week"
      },
      {
       "value": "significant_month",
       "label": "Significant · past 30 days"
      }
     ],
     "group": "Data"
    },
    {
     "key": "min_mag",
     "title": "Minimum magnitude",
     "type": "number",
     "description": "Hide smaller events (on top of the feed's own threshold)",
     "default": 0.0,
     "min": 0.0,
     "max": 9.0,
     "group": "Data"
    },
    {
     "key": "radius_km",
     "title": "Near-me radius (km)",
     "type": "integer",
     "description": "Used by the regional map ring and the alert",
     "default": 1000,
     "min": 50,
     "max": 5000,
     "group": "Data"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "world",
     "options": [
      {
       "value": "world",
       "label": "World map"
      },
      {
       "value": "region",
       "label": "Regional map"
      },
      {
       "value": "card",
       "label": "Card"
      },
      {
       "value": "list",
       "label": "List"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "focus",
     "title": "Highlight",
     "type": "choice",
     "description": "Which quakes the card, the map rings and the list feature",
     "default": "latest",
     "options": [
      {
       "value": "latest",
       "label": "Latest"
      },
      {
       "value": "nearest",
       "label": "Nearest"
      },
      {
       "value": "strongest",
       "label": "Strongest"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "center",
     "title": "World map centre",
     "type": "choice",
     "default": "home",
     "options": [
      {
       "value": "home",
       "label": "My longitude"
      },
      {
       "value": "pacific",
       "label": "Pacific (150°E)"
      },
      {
       "value": "greenwich",
       "label": "Greenwich (0°)"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "highlights",
     "title": "Quakes featured",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Layout"
    },
    {
     "key": "color_by",
     "title": "Colour by",
     "type": "choice",
     "default": "magnitude",
     "options": [
      {
       "value": "magnitude",
       "label": "Magnitude"
      },
      {
       "value": "depth",
       "label": "Depth"
      },
      {
       "value": "age",
       "label": "Age"
      }
     ],
     "group": "Colours"
    },
    {
     "key": "land",
     "title": "Land",
     "type": "color",
     "default": "#343a4a",
     "group": "Colours"
    },
    {
     "key": "home_color",
     "title": "You are here",
     "type": "color",
     "default": "#00dcff",
     "group": "Colours"
    },
    {
     "key": "pulse",
     "title": "Pulsing rings",
     "type": "toggle",
     "default": true,
     "group": "Colours"
    },
    {
     "key": "alert",
     "title": "Alert on nearby quakes",
     "type": "toggle",
     "default": true,
     "group": "Alerts"
    },
    {
     "key": "alert_mag",
     "title": "Alert magnitude",
     "type": "number",
     "default": 4.5,
     "min": 1.0,
     "max": 9.0,
     "group": "Alerts"
    }
   ]
  },
  {
   "id": "radar",
   "name": "Flight Radar",
   "category": "data",
   "icon": "radar",
   "description": "A phosphor radar scope of live aircraft around you (free ADS-B). Click a blip to inspect it.",
   "kind": "clip",
   "kinds": [
    "clip",
    "stream"
   ],
   "page": "apps/radar.html",
   "preview": "media/apps/radar.gif",
   "poster": "media/apps/radar.png",
   "og_image": "media/apps/og/radar.png",
   "source": "src/deskdot/apps/radar.py",
   "actions": [
    {
     "id": "next",
     "label": "Next aircraft"
    },
    {
     "id": "clear",
     "label": "Clear selection"
    }
   ],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "scope",
     "options": [
      {
       "value": "scope",
       "label": "Radar scope"
      },
      {
       "value": "list",
       "label": "Departures list"
      }
     ],
     "group": "Scope"
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "green",
     "options": [
      {
       "value": "green",
       "label": "Green phosphor"
      },
      {
       "value": "amber",
       "label": "Amber"
      },
      {
       "value": "cyan",
       "label": "Cyan"
      },
      {
       "value": "military",
       "label": "Military grid"
      }
     ],
     "group": "Scope"
    },
    {
     "key": "range_nm",
     "title": "Range (nm)",
     "type": "integer",
     "default": 30,
     "min": 5,
     "max": 150,
     "group": "Scope"
    },
    {
     "key": "sweep_speed",
     "title": "Sweep speed (rpm)",
     "type": "number",
     "default": 12.0,
     "min": 2.0,
     "max": 30.0,
     "group": "Scope"
    },
    {
     "key": "trails",
     "title": "Heading trails",
     "type": "toggle",
     "default": true,
     "group": "Scope"
    },
    {
     "key": "show_labels",
     "title": "Show callsign",
     "type": "toggle",
     "description": "Callsign, altitude and distance of the focused aircraft",
     "default": true,
     "group": "Labels"
    },
    {
     "key": "highlight",
     "title": "Highlight",
     "type": "choice",
     "default": "nearest",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "nearest",
       "label": "Nearest"
      },
      {
       "value": "fastest",
       "label": "Fastest"
      },
      {
       "value": "highest",
       "label": "Highest"
      }
     ],
     "group": "Labels"
    },
    {
     "key": "rotate_labels",
     "title": "Cycle labels",
     "type": "toggle",
     "description": "Step the label through every aircraft",
     "default": false,
     "group": "Labels"
    },
    {
     "key": "min_altitude_ft",
     "title": "Min altitude (ft)",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 40000,
     "group": "Filters"
    },
    {
     "key": "hide_ground",
     "title": "Hide aircraft on the ground",
     "type": "toggle",
     "default": true,
     "group": "Filters"
    }
   ]
  },
  {
   "id": "github",
   "name": "GitHub Graph",
   "category": "data",
   "icon": "git-commit-horizontal",
   "description": "Your contribution graph, total and streak — straight from your public profile.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/github.html",
   "preview": "media/apps/github.gif",
   "poster": "media/apps/github.png",
   "og_image": "media/apps/og/github.png",
   "source": "src/deskdot/apps/extras.py",
   "actions": [],
   "settings": [
    {
     "key": "user",
     "title": "GitHub username",
     "type": "text",
     "default": "torvalds",
     "max_length": 39
    },
    {
     "key": "weeks",
     "title": "Weeks",
     "type": "choice",
     "default": "16",
     "options": [
      {
       "value": "16",
       "label": "16 weeks (wide cells)"
      },
      {
       "value": "32",
       "label": "32 weeks"
      }
     ]
    },
    {
     "key": "palette",
     "title": "Palette",
     "type": "choice",
     "default": "green",
     "options": [
      {
       "value": "green",
       "label": "GitHub green"
      },
      {
       "value": "ember",
       "label": "Ember"
      },
      {
       "value": "ice",
       "label": "Ice"
      }
     ]
    }
   ]
  },
  {
   "id": "headlines",
   "name": "Headlines",
   "category": "data",
   "icon": "newspaper",
   "description": "Hacker News, Spaceflight News, DEV and Lobsters: story cards, big points or a vertical ticker.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/headlines.html",
   "preview": "media/apps/headlines.gif",
   "poster": "media/apps/headlines.png",
   "og_image": "media/apps/og/headlines.png",
   "source": "src/deskdot/apps/headlines.py",
   "actions": [
    {
     "id": "next",
     "label": "Next story"
    }
   ],
   "settings": [
    {
     "key": "source",
     "title": "Source",
     "type": "choice",
     "default": "hn",
     "options": [
      {
       "value": "hn",
       "label": "Hacker News"
      },
      {
       "value": "space",
       "label": "Spaceflight News"
      },
      {
       "value": "devto",
       "label": "DEV Community"
      },
      {
       "value": "lobsters",
       "label": "Lobsters"
      }
     ],
     "group": "News"
    },
    {
     "key": "feed",
     "title": "Feed",
     "type": "choice",
     "description": "Hacker News: all five · DEV: top (day) / best (week) / new · Lobsters: hottest / active / newest",
     "default": "top",
     "options": [
      {
       "value": "top",
       "label": "Top"
      },
      {
       "value": "best",
       "label": "Best"
      },
      {
       "value": "new",
       "label": "New"
      },
      {
       "value": "ask",
       "label": "Ask"
      },
      {
       "value": "show",
       "label": "Show"
      }
     ],
     "group": "News"
    },
    {
     "key": "count",
     "title": "Stories",
     "type": "integer",
     "default": 10,
     "min": 1,
     "max": 30,
     "group": "News"
    },
    {
     "key": "min_points",
     "title": "Minimum points",
     "type": "integer",
     "description": "Skip stories below this score (sources without scores ignore it)",
     "default": 0,
     "min": 0,
     "max": 2000,
     "group": "News"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "card",
     "options": [
      {
       "value": "card",
       "label": "Story card"
      },
      {
       "value": "big",
       "label": "Big number"
      },
      {
       "value": "ticker",
       "label": "Vertical ticker"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "title_style",
     "title": "Title",
     "type": "choice",
     "default": "marquee",
     "options": [
      {
       "value": "marquee",
       "label": "Marquee"
      },
      {
       "value": "wrap",
       "label": "Word-wrapped pages"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per story",
     "type": "integer",
     "description": "Minimum; a scrolling title always finishes its pass",
     "default": 10,
     "min": 3,
     "max": 120,
     "group": "Layout"
    },
    {
     "key": "speed",
     "title": "Scroll speed (px/s)",
     "type": "integer",
     "default": 12,
     "min": 4,
     "max": 24,
     "group": "Layout"
    },
    {
     "key": "colors",
     "title": "Accent",
     "type": "choice",
     "default": "source",
     "options": [
      {
       "value": "source",
       "label": "Source colour"
      },
      {
       "value": "custom",
       "label": "Custom"
      }
     ],
     "group": "Colours"
    },
    {
     "key": "accent",
     "title": "Custom accent",
     "type": "color",
     "default": "#ff6600",
     "group": "Colours"
    },
    {
     "key": "text_color",
     "title": "Title",
     "type": "color",
     "default": "#ffffff",
     "group": "Colours"
    }
   ]
  },
  {
   "id": "hassentity",
   "name": "Home Assistant",
   "category": "data",
   "icon": "house",
   "description": "Any Home Assistant entity: state, icon, unit, thresholds and a history sparkline.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/hassentity.html",
   "preview": "media/apps/hassentity.gif",
   "poster": "media/apps/hassentity.png",
   "og_image": "media/apps/og/hassentity.png",
   "source": "src/deskdot/apps/hassentity.py",
   "actions": [],
   "settings": [
    {
     "key": "entity",
     "title": "Entity id",
     "type": "text",
     "default": "sensor.living_room_temperature",
     "max_length": 120
    },
    {
     "key": "label",
     "title": "Label",
     "type": "text",
     "description": "Empty = the entity's friendly name",
     "default": "",
     "max_length": 24
    },
    {
     "key": "icon",
     "title": "Icon",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Automatic"
      },
      {
       "value": "temp",
       "label": "Thermometer"
      },
      {
       "value": "drop",
       "label": "Drop"
      },
      {
       "value": "bolt",
       "label": "Bolt"
      },
      {
       "value": "sun",
       "label": "Sun"
      },
      {
       "value": "bulb",
       "label": "Bulb"
      },
      {
       "value": "battery",
       "label": "Battery"
      },
      {
       "value": "door",
       "label": "Door"
      },
      {
       "value": "lock",
       "label": "Lock"
      },
      {
       "value": "person",
       "label": "Person"
      },
      {
       "value": "home",
       "label": "Home"
      },
      {
       "value": "fire",
       "label": "Flame"
      },
      {
       "value": "none",
       "label": "None"
      }
     ]
    },
    {
     "key": "unit",
     "title": "Unit",
     "type": "text",
     "description": "Empty = the unit Home Assistant reports",
     "default": "",
     "max_length": 8
    },
    {
     "key": "decimals",
     "title": "Decimals",
     "type": "integer",
     "default": 1,
     "min": 0,
     "max": 3
    },
    {
     "key": "color",
     "title": "Colour",
     "type": "color",
     "default": "#00dcff"
    },
    {
     "key": "warn_at",
     "title": "Amber at",
     "type": "text",
     "description": "Number; empty = off",
     "default": "",
     "max_length": 16
    },
    {
     "key": "alert_at",
     "title": "Red at",
     "type": "text",
     "description": "Number; empty = off",
     "default": "",
     "max_length": 16
    },
    {
     "key": "direction",
     "title": "Thresholds trigger",
     "type": "choice",
     "default": "above",
     "options": [
      {
       "value": "above",
       "label": "At or above"
      },
      {
       "value": "below",
       "label": "At or below"
      }
     ]
    },
    {
     "key": "poll",
     "title": "Refresh every (s)",
     "type": "integer",
     "default": 15,
     "min": 5,
     "max": 600
    },
    {
     "key": "history_h",
     "title": "History (hours)",
     "type": "integer",
     "description": "0 hides the sparkline",
     "default": 6,
     "min": 0,
     "max": 48
    }
   ]
  },
  {
   "id": "scores",
   "name": "Live Scores",
   "category": "data",
   "icon": "trophy",
   "description": "NBA, NFL, MLB, NHL, soccer, cricket, tennis, golf, F1 and UFC — live, in team colours.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/scores.html",
   "preview": "media/apps/scores.gif",
   "poster": "media/apps/scores.png",
   "og_image": "media/apps/og/scores.png",
   "source": "src/deskdot/apps/scores.py",
   "actions": [],
   "settings": [
    {
     "key": "league",
     "title": "League",
     "type": "choice",
     "description": "Used when 'Leagues' is empty",
     "default": "nba",
     "options": [
      {
       "value": "nba",
       "label": "NBA"
      },
      {
       "value": "wnba",
       "label": "WNBA"
      },
      {
       "value": "ncaab",
       "label": "NCAAB"
      },
      {
       "value": "nfl",
       "label": "NFL"
      },
      {
       "value": "ncaaf",
       "label": "NCAAF"
      },
      {
       "value": "mlb",
       "label": "MLB"
      },
      {
       "value": "nhl",
       "label": "NHL"
      },
      {
       "value": "epl",
       "label": "EPL"
      },
      {
       "value": "laliga",
       "label": "LIGA"
      },
      {
       "value": "seriea",
       "label": "SERIE A"
      },
      {
       "value": "bundesliga",
       "label": "BUNDES"
      },
      {
       "value": "ligue1",
       "label": "LIGUE 1"
      },
      {
       "value": "ucl",
       "label": "UCL"
      },
      {
       "value": "uel",
       "label": "UEL"
      },
      {
       "value": "worldcup",
       "label": "WORLD CUP"
      },
      {
       "value": "mls",
       "label": "MLS"
      },
      {
       "value": "isl",
       "label": "ISL"
      },
      {
       "value": "ipl",
       "label": "IPL"
      },
      {
       "value": "bbl",
       "label": "BBL"
      },
      {
       "value": "psl",
       "label": "PSL"
      },
      {
       "value": "cricket",
       "label": "CRICKET"
      },
      {
       "value": "atp",
       "label": "ATP"
      },
      {
       "value": "wta",
       "label": "WTA"
      },
      {
       "value": "pga",
       "label": "PGA"
      },
      {
       "value": "lpga",
       "label": "LPGA"
      },
      {
       "value": "f1",
       "label": "F1"
      },
      {
       "value": "ufc",
       "label": "UFC"
      }
     ],
     "group": "Leagues"
    },
    {
     "key": "leagues",
     "title": "Leagues",
     "type": "text",
     "description": "Comma list, several at once: e.g. nba,epl,ipl,f1. Options: nba, wnba, ncaab, nfl, ncaaf, mlb, nhl, epl, laliga, seriea, bundesliga, ligue1, ucl, uel, worldcup, mls, isl, ipl, bbl, psl, cricket, atp, wta, pga, lpga, f1, ufc",
     "default": "",
     "max_length": 200,
     "group": "Leagues"
    },
    {
     "key": "team",
     "title": "Favourite teams",
     "type": "text",
     "description": "Comma list of abbreviations or player surnames, e.g. LAL,ARS,MI,SINNER",
     "default": "",
     "max_length": 80,
     "group": "Leagues"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "focus",
     "options": [
      {
       "value": "focus",
       "label": "One game at a time"
      },
      {
       "value": "all",
       "label": "All games (scroll)"
      },
      {
       "value": "live_only",
       "label": "Live games only"
      },
      {
       "value": "favorites",
       "label": "Favourites only"
      },
      {
       "value": "table",
       "label": "League table"
      }
     ],
     "group": "Display"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "description": "Team sports; tennis, golf, F1 and UFC always use their own card",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "hero",
       "label": "Split hero"
      },
      {
       "value": "big_score",
       "label": "Big score"
      },
      {
       "value": "compact",
       "label": "Compact (2 games)"
      },
      {
       "value": "ticker",
       "label": "Ticker (all games)"
      }
     ],
     "group": "Display"
    },
    {
     "key": "rotate",
     "title": "Seconds per game",
     "type": "integer",
     "default": 6,
     "min": 3,
     "max": 60,
     "group": "Display"
    },
    {
     "key": "time_format",
     "title": "Upcoming games",
     "type": "choice",
     "default": "12h",
     "options": [
      {
       "value": "12h",
       "label": "7:30P"
      },
      {
       "value": "24h",
       "label": "19:30"
      },
      {
       "value": "countdown",
       "label": "IN 2H"
      }
     ],
     "group": "Display"
    },
    {
     "key": "show_logos_colors",
     "title": "Team colours",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "show_clock",
     "title": "Game clock",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "show_records",
     "title": "Team records",
     "type": "toggle",
     "default": false,
     "group": "Display"
    },
    {
     "key": "show_icons",
     "title": "Sport icons",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "scroll_direction",
     "title": "Scroll direction",
     "type": "choice",
     "default": "vertical",
     "options": [
      {
       "value": "vertical",
       "label": "Vertical"
      },
      {
       "value": "horizontal",
       "label": "Horizontal"
      }
     ],
     "group": "Scroll"
    },
    {
     "key": "scroll_speed",
     "title": "Scroll speed",
     "type": "integer",
     "description": "Pixels per second",
     "default": 10,
     "min": 4,
     "max": 30,
     "group": "Scroll"
    },
    {
     "key": "take_over",
     "title": "Take over when favourite is live",
     "type": "toggle",
     "default": false,
     "group": "Alerts"
    },
    {
     "key": "alerts",
     "title": "Goal / score alerts",
     "type": "choice",
     "description": "Celebration overlay, even when not on screen",
     "default": "favorite",
     "options": [
      {
       "value": "off",
       "label": "Off"
      },
      {
       "value": "favorite",
       "label": "Favourite team"
      },
      {
       "value": "all",
       "label": "All games"
      }
     ],
     "group": "Alerts"
    }
   ]
  },
  {
   "id": "planets",
   "name": "Planets Tonight",
   "category": "data",
   "icon": "orbit",
   "description": "Which naked-eye planets are up now or tonight, on a sky dome or a list with rise/set times.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/planets.html",
   "preview": "media/apps/planets.gif",
   "poster": "media/apps/planets.png",
   "og_image": "media/apps/og/planets.png",
   "source": "src/deskdot/apps/planets.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "dome",
     "options": [
      {
       "value": "dome",
       "label": "Sky dome"
      },
      {
       "value": "list",
       "label": "List"
      }
     ]
    },
    {
     "key": "when",
     "title": "Sky at",
     "type": "choice",
     "default": "tonight",
     "options": [
      {
       "value": "tonight",
       "label": "Tonight"
      },
      {
       "value": "now",
       "label": "Right now"
      }
     ]
    },
    {
     "key": "show_moon",
     "title": "Show the moon",
     "type": "toggle",
     "default": true
    },
    {
     "key": "show_below",
     "title": "List planets below the horizon",
     "type": "toggle",
     "description": "List layout.",
     "default": false
    },
    {
     "key": "hour24",
     "title": "24-hour times",
     "type": "toggle",
     "default": true
    },
    {
     "key": "page_seconds",
     "title": "Seconds per label",
     "type": "integer",
     "default": 3,
     "min": 2,
     "max": 15
    }
   ]
  },
  {
   "id": "rainradar",
   "name": "Rain Radar",
   "category": "data",
   "icon": "cloud-rain",
   "description": "Animated rain radar around you (past 2 hours), with a home marker and coastline.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/rainradar.html",
   "preview": "media/apps/rainradar.gif",
   "poster": "media/apps/rainradar.png",
   "og_image": "media/apps/og/rainradar.png",
   "source": "src/deskdot/apps/rainradar.py",
   "actions": [],
   "settings": [
    {
     "key": "zoom",
     "title": "Area (width)",
     "type": "choice",
     "default": "6",
     "options": [
      {
       "value": "7",
       "label": "300 km"
      },
      {
       "value": "6",
       "label": "600 km"
      },
      {
       "value": "5",
       "label": "1 200 km"
      },
      {
       "value": "4",
       "label": "2 400 km"
      },
      {
       "value": "3",
       "label": "4 800 km"
      }
     ],
     "group": "Map"
    },
    {
     "key": "basemap",
     "title": "Basemap",
     "type": "choice",
     "default": "outline",
     "options": [
      {
       "value": "outline",
       "label": "Coastline"
      },
      {
       "value": "terrain",
       "label": "Terrain"
      },
      {
       "value": "land",
       "label": "Land fill"
      },
      {
       "value": "none",
       "label": "None"
      }
     ],
     "group": "Map"
    },
    {
     "key": "coverage",
     "title": "Mark areas without radar",
     "type": "toggle",
     "default": true,
     "group": "Map"
    },
    {
     "key": "home",
     "title": "Home marker",
     "type": "toggle",
     "default": true,
     "group": "Map"
    },
    {
     "key": "scheme",
     "title": "Colour scheme",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "blue",
       "label": "Blue"
      },
      {
       "value": "heat",
       "label": "Heat"
      },
      {
       "value": "mono",
       "label": "Mono"
      }
     ],
     "group": "Colours"
    },
    {
     "key": "mono_color",
     "title": "Mono colour",
     "type": "color",
     "default": "#00dcff",
     "group": "Colours"
    },
    {
     "key": "home_color",
     "title": "Home colour",
     "type": "color",
     "default": "#ffffff",
     "group": "Colours"
    },
    {
     "key": "threshold",
     "title": "Minimum intensity (dBZ)",
     "type": "integer",
     "description": "Hide weaker echoes: 10 ≈ drizzle, 20 light rain, 30 moderate",
     "default": 10,
     "min": -10,
     "max": 40,
     "group": "Colours"
    },
    {
     "key": "speed",
     "title": "Loop speed",
     "type": "choice",
     "default": "normal",
     "options": [
      {
       "value": "slow",
       "label": "Slow"
      },
      {
       "value": "normal",
       "label": "Normal"
      },
      {
       "value": "fast",
       "label": "Fast"
      }
     ],
     "group": "Loop"
    },
    {
     "key": "smooth",
     "title": "Cross-fade frames",
     "type": "toggle",
     "default": true,
     "group": "Loop"
    },
    {
     "key": "time_label",
     "title": "Frame time",
     "type": "choice",
     "default": "clock",
     "options": [
      {
       "value": "clock",
       "label": "Clock time"
      },
      {
       "value": "relative",
       "label": "Minutes ago"
      },
      {
       "value": "off",
       "label": "Off"
      }
     ],
     "group": "Loop"
    },
    {
     "key": "hour24",
     "title": "24-hour clock",
     "type": "toggle",
     "default": true,
     "group": "Loop"
    }
   ]
  },
  {
   "id": "space",
   "name": "Space",
   "category": "data",
   "icon": "rocket",
   "description": "Next rocket launch countdown, live ISS tracker, people in orbit and spaceflight news.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/space.html",
   "preview": "media/apps/space.gif",
   "poster": "media/apps/space.png",
   "og_image": "media/apps/og/space.png",
   "source": "src/deskdot/apps/space.py",
   "actions": [
    {
     "id": "next",
     "label": "Next screen"
    }
   ],
   "settings": [
    {
     "key": "mode",
     "title": "Screen",
     "type": "choice",
     "default": "rotate",
     "options": [
      {
       "value": "launch",
       "label": "Next launch"
      },
      {
       "value": "iss",
       "label": "ISS tracker"
      },
      {
       "value": "people",
       "label": "People in space"
      },
      {
       "value": "news",
       "label": "Space news"
      },
      {
       "value": "rotate",
       "label": "Rotate all"
      }
     ],
     "group": "Screen"
    },
    {
     "key": "rotate_seconds",
     "title": "Seconds per screen",
     "type": "integer",
     "default": 15,
     "min": 5,
     "max": 120,
     "group": "Screen"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#ff4818",
     "group": "Screen"
    },
    {
     "key": "launch_filter",
     "title": "Launch provider",
     "type": "choice",
     "default": "any",
     "options": [
      {
       "value": "any",
       "label": "Any provider"
      },
      {
       "value": "spacex",
       "label": "SpaceX"
      },
      {
       "value": "rocketlab",
       "label": "Rocket Lab"
      },
      {
       "value": "isro",
       "label": "ISRO"
      },
      {
       "value": "nasa",
       "label": "NASA"
      },
      {
       "value": "casc",
       "label": "China (CASC)"
      },
      {
       "value": "roscosmos",
       "label": "Roscosmos"
      },
      {
       "value": "ula",
       "label": "ULA"
      },
      {
       "value": "blueorigin",
       "label": "Blue Origin"
      },
      {
       "value": "arianespace",
       "label": "Arianespace"
      },
      {
       "value": "mhi",
       "label": "Japan (MHI)"
      }
     ],
     "group": "Launches"
    },
    {
     "key": "hour24",
     "title": "24-hour times",
     "type": "toggle",
     "default": true,
     "group": "Launches"
    },
    {
     "key": "take_over",
     "title": "Take over for launches",
     "type": "toggle",
     "description": "Jump to the countdown for the final minute and the liftoff",
     "default": false,
     "group": "Launches"
    },
    {
     "key": "iss_view",
     "title": "ISS view",
     "type": "choice",
     "default": "follow",
     "options": [
      {
       "value": "follow",
       "label": "Follow-cam half globe"
      },
      {
       "value": "world",
       "label": "Whole world + readout"
      }
     ],
     "group": "ISS"
    },
    {
     "key": "iss_track",
     "title": "Ground track behind",
     "type": "toggle",
     "default": true,
     "group": "ISS"
    },
    {
     "key": "iss_ahead",
     "title": "Predicted track ahead",
     "type": "toggle",
     "default": true,
     "group": "ISS"
    },
    {
     "key": "iss_night",
     "title": "Day / night shading",
     "type": "toggle",
     "default": true,
     "group": "ISS"
    },
    {
     "key": "units",
     "title": "Units",
     "type": "choice",
     "default": "km",
     "options": [
      {
       "value": "km",
       "label": "Kilometres"
      },
      {
       "value": "mi",
       "label": "Miles"
      }
     ],
     "group": "ISS"
    },
    {
     "key": "news_seconds",
     "title": "Seconds per headline page",
     "type": "integer",
     "default": 6,
     "min": 3,
     "max": 30,
     "group": "News"
    }
   ]
  },
  {
   "id": "stocks",
   "name": "Stocks",
   "category": "data",
   "icon": "trending-up",
   "description": "Stocks, indices, forex and futures: cards, ticker tape, watchlist, charts and heatmap.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/stocks.html",
   "preview": "media/apps/stocks.gif",
   "poster": "media/apps/stocks.png",
   "og_image": "media/apps/og/stocks.png",
   "source": "src/deskdot/apps/stocks.py",
   "actions": [],
   "settings": [
    {
     "key": "symbols",
     "title": "Symbols",
     "type": "text",
     "description": "Yahoo symbols, comma-separated: AAPL, ^GSPC, ^NSEI, TCS.NS, SPY, USDINR=X, GC=F",
     "default": "AAPL,NVDA,^NSEI,RELIANCE.NS",
     "group": "Symbols"
    },
    {
     "key": "label",
     "title": "Labels",
     "type": "choice",
     "default": "short",
     "options": [
      {
       "value": "short",
       "label": "Short"
      },
      {
       "value": "ticker",
       "label": "Ticker"
      },
      {
       "value": "name",
       "label": "Name"
      }
     ],
     "group": "Symbols"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "card",
     "options": [
      {
       "value": "card",
       "label": "Card"
      },
      {
       "value": "ticker",
       "label": "Ticker tape"
      },
      {
       "value": "watchlist",
       "label": "Watchlist"
      },
      {
       "value": "chart",
       "label": "Chart"
      },
      {
       "value": "heatmap",
       "label": "Heatmap"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "range",
     "title": "Chart range",
     "type": "choice",
     "default": "1d",
     "options": [
      {
       "value": "1d",
       "label": "1D"
      },
      {
       "value": "5d",
       "label": "5D"
      },
      {
       "value": "1mo",
       "label": "1MO"
      },
      {
       "value": "6mo",
       "label": "6MO"
      },
      {
       "value": "1y",
       "label": "1Y"
      },
      {
       "value": "5y",
       "label": "5Y"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "chart",
     "title": "Chart style",
     "type": "choice",
     "default": "area",
     "options": [
      {
       "value": "line",
       "label": "Line"
      },
      {
       "value": "area",
       "label": "Area"
      },
      {
       "value": "candles",
       "label": "Candles"
      },
      {
       "value": "bars",
       "label": "Bars"
      },
      {
       "value": "dots",
       "label": "Dots"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "show",
     "title": "Change",
     "type": "choice",
     "default": "percent",
     "options": [
      {
       "value": "percent",
       "label": "Percent"
      },
      {
       "value": "absolute",
       "label": "Absolute"
      },
      {
       "value": "both",
       "label": "Both"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per symbol",
     "type": "integer",
     "default": 8,
     "min": 2,
     "max": 120,
     "group": "Layout"
    },
    {
     "key": "speed",
     "title": "Ticker speed (px/s)",
     "type": "integer",
     "default": 12,
     "min": 4,
     "max": 30,
     "group": "Layout"
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "vivid",
       "label": "Vivid"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "inverse",
       "label": "Inverse (red up)"
      }
     ],
     "group": "Colours"
    },
    {
     "key": "up",
     "title": "Up colour",
     "type": "color",
     "default": "#00ff78",
     "group": "Colours"
    },
    {
     "key": "down",
     "title": "Down colour",
     "type": "color",
     "default": "#ff143c",
     "group": "Colours"
    },
    {
     "key": "neutral",
     "title": "Neutral colour",
     "type": "color",
     "default": "#ffaa00",
     "group": "Colours"
    },
    {
     "key": "heat_scale",
     "title": "Full-scale change %",
     "type": "number",
     "description": "Change that saturates heatmap tiles and watchlist bars",
     "default": 3.0,
     "min": 0.5,
     "max": 10.0,
     "group": "Colours"
    },
    {
     "key": "show_market_state",
     "title": "Market state dot",
     "type": "toggle",
     "description": "Green = open (blinks), amber = pre, violet = post, grey = closed",
     "default": true,
     "group": "Details"
    },
    {
     "key": "show_currency",
     "title": "Currency sign",
     "type": "toggle",
     "default": false,
     "group": "Details"
    },
    {
     "key": "compact",
     "title": "Compact numbers",
     "type": "toggle",
     "description": "23446.8 → 23.4K",
     "default": false,
     "group": "Details"
    },
    {
     "key": "decimals",
     "title": "Decimals",
     "type": "integer",
     "description": "-1 = the exchange's own precision",
     "default": -1,
     "min": -1,
     "max": 4,
     "group": "Details"
    },
    {
     "key": "alert_above",
     "title": "Alert above",
     "type": "number",
     "description": "Price of the first symbol; 0 = off",
     "default": 0.0,
     "min": 0,
     "group": "Alerts"
    },
    {
     "key": "alert_below",
     "title": "Alert below",
     "type": "number",
     "description": "Price of the first symbol; 0 = off",
     "default": 0.0,
     "min": 0,
     "group": "Alerts"
    }
   ]
  },
  {
   "id": "sysmon",
   "name": "System Monitor",
   "category": "data",
   "icon": "cpu",
   "description": "Live CPU, memory, storage and network from this PC.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/sysmon.html",
   "preview": "media/apps/sysmon.gif",
   "poster": "media/apps/sysmon.png",
   "og_image": "media/apps/og/sysmon.png",
   "source": "src/deskdot/apps/sysmon.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "bars",
     "options": [
      {
       "value": "bars",
       "label": "Bars"
      },
      {
       "value": "graph",
       "label": "CPU graph"
      }
     ]
    },
    {
     "key": "cpu_color",
     "title": "CPU",
     "type": "color",
     "default": "#00ff8c"
    },
    {
     "key": "ram_color",
     "title": "RAM",
     "type": "color",
     "default": "#ff00be"
    },
    {
     "key": "disk_color",
     "title": "Disk",
     "type": "color",
     "default": "#ffaa00"
    }
   ]
  },
  {
   "id": "tides",
   "name": "Tides & Surf",
   "category": "data",
   "icon": "waves-horizontal",
   "description": "Tide curve with a now marker and next high/low, plus wave height, period and direction.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/tides.html",
   "preview": "media/apps/tides.gif",
   "poster": "media/apps/tides.png",
   "og_image": "media/apps/og/tides.png",
   "source": "src/deskdot/apps/tides.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "tide",
     "options": [
      {
       "value": "tide",
       "label": "Tide curve"
      },
      {
       "value": "surf",
       "label": "Surf"
      },
      {
       "value": "both",
       "label": "Both (alternate)"
      }
     ]
    },
    {
     "key": "hours_back",
     "title": "Hours before now on the curve",
     "type": "integer",
     "default": 6,
     "min": 0,
     "max": 12
    },
    {
     "key": "hours_ahead",
     "title": "Hours after now on the curve",
     "type": "integer",
     "default": 18,
     "min": 6,
     "max": 36
    },
    {
     "key": "page_seconds",
     "title": "Seconds per page (both)",
     "type": "integer",
     "default": 8,
     "min": 3,
     "max": 60
    },
    {
     "key": "coast_lat",
     "title": "Coast latitude",
     "type": "number",
     "description": "0/0 = your location.",
     "default": 0.0,
     "min": -90,
     "max": 90,
     "group": "Coast"
    },
    {
     "key": "coast_lon",
     "title": "Coast longitude",
     "type": "number",
     "default": 0.0,
     "min": -180,
     "max": 180,
     "group": "Coast"
    },
    {
     "key": "hour24",
     "title": "24-hour times",
     "type": "toggle",
     "default": true
    }
   ]
  },
  {
   "id": "weather",
   "name": "Weather",
   "category": "data",
   "icon": "cloud-sun",
   "description": "Animated conditions, temperature, high/low and a 24-hour trend. Location in Settings.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/weather.html",
   "preview": "media/apps/weather.gif",
   "poster": "media/apps/weather.png",
   "og_image": "media/apps/og/weather.png",
   "source": "src/deskdot/apps/weather.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "icon",
     "options": [
      {
       "value": "icon",
       "label": "Icon + condition"
      },
      {
       "value": "forecast",
       "label": "Icon + 24h trend"
      }
     ]
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#00dcff"
    }
   ]
  },
  {
   "id": "wear",
   "name": "What to Wear",
   "category": "data",
   "icon": "shirt",
   "description": "One big icon for the next hours — umbrella, jacket, shades, T-shirt, scarf or snow boots.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/wear.html",
   "preview": "media/apps/wear.gif",
   "poster": "media/apps/wear.png",
   "og_image": "media/apps/og/wear.png",
   "source": "src/deskdot/apps/wear.py",
   "actions": [],
   "settings": [
    {
     "key": "icon",
     "title": "Icon",
     "type": "choice",
     "description": "Leave on 'From forecast'; the others pin one icon (handy for testing).",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "From forecast"
      },
      {
       "value": "umbrella",
       "label": "Umbrella"
      },
      {
       "value": "jacket",
       "label": "Jacket"
      },
      {
       "value": "sunglasses",
       "label": "Sunglasses"
      },
      {
       "value": "tshirt",
       "label": "T-shirt"
      },
      {
       "value": "scarf",
       "label": "Scarf"
      },
      {
       "value": "boots",
       "label": "Snow boots"
      }
     ]
    },
    {
     "key": "hours",
     "title": "Look ahead (hours)",
     "type": "integer",
     "default": 6,
     "min": 2,
     "max": 12,
     "group": "Thresholds"
    },
    {
     "key": "rain_chance",
     "title": "Umbrella from rain chance (%)",
     "type": "integer",
     "default": 50,
     "min": 10,
     "max": 100,
     "group": "Thresholds"
    },
    {
     "key": "scarf_below",
     "title": "Scarf below (feels-like °C)",
     "type": "integer",
     "default": 5,
     "min": -20,
     "max": 20,
     "group": "Thresholds"
    },
    {
     "key": "jacket_below",
     "title": "Jacket below (feels-like °C)",
     "type": "integer",
     "default": 15,
     "min": -10,
     "max": 30,
     "group": "Thresholds"
    },
    {
     "key": "uv_from",
     "title": "Sunglasses from UV index",
     "type": "integer",
     "default": 6,
     "min": 1,
     "max": 12,
     "group": "Thresholds"
    }
   ]
  },
  {
   "id": "printer",
   "name": "3D Printer",
   "category": "device",
   "icon": "printer",
   "description": "Print progress ring, ETA, nozzle and bed temperatures from OctoPrint or Moonraker.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/printer.html",
   "preview": "media/apps/printer.gif",
   "poster": "media/apps/printer.png",
   "og_image": "media/apps/og/printer.png",
   "source": "src/deskdot/apps/printer.py",
   "actions": [],
   "settings": [
    {
     "key": "kind",
     "title": "Server",
     "type": "choice",
     "default": "octoprint",
     "options": [
      {
       "value": "octoprint",
       "label": "OctoPrint"
      },
      {
       "value": "moonraker",
       "label": "Moonraker / Klipper"
      }
     ],
     "group": "Connection"
    },
    {
     "key": "host",
     "title": "Host",
     "type": "text",
     "description": "e.g. octopi.local or 192.168.1.50 (blank = not set up)",
     "default": "",
     "max_length": 120,
     "group": "Connection"
    },
    {
     "key": "port",
     "title": "Port",
     "type": "integer",
     "description": "0 = default (OctoPrint 80, Moonraker 7125)",
     "default": 0,
     "min": 0,
     "max": 65535,
     "group": "Connection"
    },
    {
     "key": "api_key",
     "title": "API key",
     "type": "text",
     "description": "OctoPrint: Settings → Application Keys. Moonraker: only if required. Never logged.",
     "default": "",
     "max_length": 120,
     "group": "Connection"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "ring",
     "options": [
      {
       "value": "ring",
       "label": "Progress ring"
      },
      {
       "value": "rows",
       "label": "Rows"
      }
     ],
     "group": "Display"
    },
    {
     "key": "celebrate_min",
     "title": "Celebrate for (min)",
     "type": "integer",
     "description": "Confetti after a finished print",
     "default": 10,
     "min": 0,
     "max": 120,
     "group": "Display"
    },
    {
     "key": "alerts",
     "title": "Notify when done or failed",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "takeover",
     "title": "Take over the playlist while printing",
     "type": "toggle",
     "default": false,
     "group": "Display"
    }
   ]
  },
  {
   "id": "native",
   "name": "Firmware Modes",
   "category": "device",
   "icon": "microchip",
   "description": "The panel's own clock designs, effects, scoreboard and timers — no streaming.",
   "kind": "native",
   "kinds": [
    "native"
   ],
   "page": "apps/native.html",
   "preview": "media/apps/native.gif",
   "poster": "media/apps/native.png",
   "og_image": "media/apps/og/native.png",
   "source": "src/deskdot/apps/native.py",
   "actions": [],
   "settings": [
    {
     "key": "feature",
     "title": "Feature",
     "type": "choice",
     "default": "clock",
     "options": [
      {
       "value": "clock",
       "label": "Clock"
      },
      {
       "value": "effect",
       "label": "Effect"
      },
      {
       "value": "color",
       "label": "Solid colour"
      },
      {
       "value": "scoreboard",
       "label": "Scoreboard"
      },
      {
       "value": "countdown",
       "label": "Countdown"
      },
      {
       "value": "stopwatch",
       "label": "Stopwatch"
      }
     ]
    },
    {
     "key": "clock_style",
     "title": "Clock design (0-7)",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 7
    },
    {
     "key": "show_date",
     "title": "Show date",
     "type": "toggle",
     "default": true
    },
    {
     "key": "hour24",
     "title": "24-hour",
     "type": "toggle",
     "default": true
    },
    {
     "key": "color",
     "title": "Colour",
     "type": "color",
     "default": "#ff4818"
    },
    {
     "key": "effect",
     "title": "Effect (0-6)",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 6
    },
    {
     "key": "score_left",
     "title": "Left score",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 999
    },
    {
     "key": "score_right",
     "title": "Right score",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 999
    },
    {
     "key": "minutes",
     "title": "Countdown minutes",
     "type": "integer",
     "default": 5,
     "min": 0,
     "max": 255
    },
    {
     "key": "seconds",
     "title": "Countdown seconds",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 59
    }
   ]
  },
  {
   "id": "g2048",
   "name": "2048",
   "category": "games",
   "icon": "grid-2x2",
   "description": "Slide and merge tiles towards 2048; colour tells the value. The AI hugs a corner. Arrows slide. B in the demo opens the menu: Race on 2–4 small boards, or Relay (take turns on one board).",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/g2048.html",
   "preview": "media/apps/g2048.gif",
   "poster": "media/apps/g2048.png",
   "og_image": "media/apps/og/g2048.png",
   "source": "src/deskdot/apps/games_puzzle.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "tiles",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "tiles",
       "label": "Tiles"
      },
      {
       "value": "ice",
       "label": "Ice"
      },
      {
       "value": "ember",
       "label": "Ember"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "numbers",
     "title": "Show numbers",
     "type": "toggle",
     "default": true
    },
    {
     "key": "race_target",
     "title": "Race to tile",
     "type": "choice",
     "default": "256",
     "options": [
      {
       "value": "128",
       "label": "128"
      },
      {
       "value": "256",
       "label": "256"
      },
      {
       "value": "512",
       "label": "512"
      },
      {
       "value": "1024",
       "label": "1024"
      }
     ],
     "group": "Game"
    },
    {
     "key": "junk",
     "title": "Junk attacks",
     "type": "toggle",
     "description": "Race: merging 64 or more drops a junk tile on every rival",
     "default": true,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "race",
       "label": "Race"
      },
      {
       "value": "relay",
       "label": "Relay"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "stones",
       "label": "Stones"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "swipe",
     "dpad",
     "keyboard",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "race",
      "name": "Race",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "relay",
      "name": "Relay",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Classic"
     },
     {
      "id": "stones",
      "label": "Stones"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "tiles",
      "label": "Tiles"
     },
     {
      "value": "ice",
      "label": "Ice"
     },
     {
      "value": "ember",
      "label": "Ember"
     }
    ]
   }
  },
  {
   "id": "raycaster",
   "name": "3D Dungeon Raycaster",
   "category": "games",
   "icon": "square",
   "description": "First-person 3D raycasting dungeon crawler with textured walls and radar.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/raycaster.html",
   "preview": "media/apps/raycaster.gif",
   "poster": "media/apps/raycaster.png",
   "og_image": "media/apps/og/raycaster.png",
   "source": "src/deskdot/apps/raycaster.py",
   "actions": [],
   "settings": [
    {
     "key": "theme",
     "title": "Visual Theme",
     "type": "choice",
     "default": "dungeon",
     "options": [
      {
       "value": "dungeon",
       "label": "Medieval Dungeon"
      },
      {
       "value": "cyberpunk",
       "label": "Cyber Grid"
      },
      {
       "value": "inferno",
       "label": "Infernal Basalt"
      }
     ]
    },
    {
     "key": "radar",
     "title": "Minimap Radar HUD",
     "type": "choice",
     "default": "on",
     "options": [
      {
       "value": "on",
       "label": "Radar HUD On"
      },
      {
       "value": "off",
       "label": "Radar HUD Off"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Exploration Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ],
   "game": {
    "max_players": 1,
    "controls": [],
    "modes": [],
    "maps": [],
    "themes": [
     {
      "value": "dungeon",
      "label": "Medieval Dungeon"
     },
     {
      "value": "cyberpunk",
      "label": "Cyber Grid"
     },
     {
      "value": "inferno",
      "label": "Infernal Basalt"
     }
    ]
   }
  },
  {
   "id": "asteroids",
   "name": "Asteroids",
   "category": "games",
   "icon": "orbit",
   "description": "Pilot a tiny ship through a field of splitting space rocks. ←/→ turn, ↑ thrust, A fire, B warp.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/asteroids.html",
   "preview": "media/apps/asteroids.gif",
   "poster": "media/apps/asteroids.png",
   "og_image": "media/apps/og/asteroids.png",
   "source": "src/deskdot/apps/games_action.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "nebula",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "nebula",
       "label": "Nebula"
      },
      {
       "value": "mars",
       "label": "Red Planet"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "rocks",
     "title": "Rocks per wave",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 6,
     "group": "Game"
    },
    {
     "key": "filled",
     "title": "Solid rocks",
     "type": "toggle",
     "default": true,
     "group": "Graphics"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "time_limit",
     "title": "Battle time (s)",
     "type": "integer",
     "description": "Dogfights end after this; most lives left wins",
     "default": 120,
     "min": 30,
     "max": 300,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "battle",
       "label": "Dogfight"
      },
      {
       "value": "teams",
       "label": "Squads"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "field",
     "options": [
      {
       "value": "field",
       "label": "Rock Field"
      },
      {
       "value": "dense",
       "label": "Dense Belt"
      },
      {
       "value": "well",
       "label": "Gravity Well"
      },
      {
       "value": "open",
       "label": "Open Space"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "gamepad",
     "dpad",
     "joystick",
     "keyboard"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     },
     {
      "id": "battle",
      "name": "Dogfight",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "teams",
      "name": "Squads",
      "min_players": 2,
      "max_players": 4,
      "teams": "versus"
     }
    ],
    "maps": [
     {
      "id": "field",
      "label": "Rock Field"
     },
     {
      "id": "dense",
      "label": "Dense Belt"
     },
     {
      "id": "well",
      "label": "Gravity Well"
     },
     {
      "id": "open",
      "label": "Open Space"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "nebula",
      "label": "Nebula"
     },
     {
      "value": "mars",
      "label": "Red Planet"
     }
    ]
   }
  },
  {
   "id": "breakout",
   "name": "Breakout",
   "category": "games",
   "icon": "brick-wall",
   "description": "Smash a colourful brick wall; new patterns every level. Left/right moves the paddle. Co-op: two paddles and two balls on one wall. Versus: a split field, first to clear their half wins.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/breakout.html",
   "preview": "media/apps/breakout.gif",
   "poster": "media/apps/breakout.png",
   "og_image": "media/apps/og/breakout.png",
   "source": "src/deskdot/apps/games_classic.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "arcade",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "arcade",
       "label": "Arcade"
      },
      {
       "value": "candy",
       "label": "Candy"
      },
      {
       "value": "lava",
       "label": "Lava"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5
    },
    {
     "key": "rows",
     "title": "Brick rows",
     "type": "integer",
     "default": 6,
     "min": 3,
     "max": 6
    },
    {
     "key": "paddle",
     "title": "Paddle width",
     "type": "integer",
     "default": 6,
     "min": 4,
     "max": 8,
     "group": "Game"
    },
    {
     "key": "drop",
     "title": "Wall drop every (s)",
     "type": "integer",
     "description": "Descend map",
     "default": 12,
     "min": 4,
     "max": 30,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "versus",
       "label": "Versus"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "fortress",
       "label": "Fortress"
      },
      {
       "value": "descend",
       "label": "Descend"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 2,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 2,
    "controls": [
     "joystick",
     "dpad",
     "tilt",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 2,
      "teams": "coop"
     },
     {
      "id": "versus",
      "name": "Versus",
      "min_players": 2,
      "max_players": 2,
      "teams": "versus"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Classic"
     },
     {
      "id": "fortress",
      "label": "Fortress"
     },
     {
      "id": "descend",
      "label": "Descend"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "arcade",
      "label": "Arcade"
     },
     {
      "value": "candy",
      "label": "Candy"
     },
     {
      "value": "lava",
      "label": "Lava"
     }
    ]
   }
  },
  {
   "id": "chess",
   "name": "Chess Puzzle",
   "category": "games",
   "icon": "crown",
   "description": "Chess.com's daily puzzle on a 32×32 board, then the solution plays out move by move.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/chess.html",
   "preview": "media/apps/chess.gif",
   "poster": "media/apps/chess.png",
   "og_image": "media/apps/og/chess.png",
   "source": "src/deskdot/apps/chess.py",
   "actions": [
    {
     "id": "next",
     "label": "Next puzzle"
    },
    {
     "id": "solve",
     "label": "Show solution"
    },
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "input",
     "label": "Step"
    }
   ],
   "settings": [
    {
     "key": "source",
     "title": "Puzzle",
     "type": "choice",
     "default": "daily",
     "options": [
      {
       "value": "daily",
       "label": "Daily puzzle"
      },
      {
       "value": "random",
       "label": "Random puzzle"
      }
     ]
    },
    {
     "key": "new_every",
     "title": "New random puzzle every (min)",
     "type": "integer",
     "description": "0 = only with the Next button",
     "default": 0,
     "min": 0,
     "max": 1440
    },
    {
     "key": "think",
     "title": "Think time (s)",
     "type": "integer",
     "description": "Before the solution plays",
     "default": 8,
     "min": 1,
     "max": 120
    },
    {
     "key": "move_seconds",
     "title": "Seconds per move",
     "type": "number",
     "default": 1.5,
     "min": 0.5,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Board",
     "type": "choice",
     "default": "night",
     "options": [
      {
       "value": "night",
       "label": "Night (LED)"
      },
      {
       "value": "wood",
       "label": "Wood"
      },
      {
       "value": "green",
       "label": "Green"
      },
      {
       "value": "blue",
       "label": "Blue"
      }
     ]
    },
    {
     "key": "orientation",
     "title": "Orientation",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Side to move at the bottom"
      },
      {
       "value": "white",
       "label": "White at the bottom"
      }
     ]
    },
    {
     "key": "intro",
     "title": "Intro card (title, side to move)",
     "type": "toggle",
     "default": true
    },
    {
     "key": "highlight",
     "title": "Highlight last move",
     "type": "toggle",
     "default": true
    }
   ],
   "game": {
    "max_players": 1,
    "controls": [],
    "modes": [],
    "maps": [],
    "themes": [
     {
      "value": "night",
      "label": "Night (LED)"
     },
     {
      "value": "wood",
      "label": "Wood"
     },
     {
      "value": "green",
      "label": "Green"
     },
     {
      "value": "blue",
      "label": "Blue"
     }
    ]
   }
  },
  {
   "id": "digworld",
   "name": "Dig World",
   "category": "games",
   "icon": "pickaxe",
   "description": "A block-world sandbox: the miner digs for ore, builds a house at dusk and hides from night creatures. Arrows walk/dig, A jumps (or places in build mode), B toggles dig/build. Two miners play co-op, up to four race for ore.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/digworld.html",
   "preview": "media/apps/digworld.gif",
   "poster": "media/apps/digworld.png",
   "og_image": "media/apps/og/digworld.png",
   "source": "src/deskdot/apps/games_world.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "overworld",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "overworld",
       "label": "Overworld"
      },
      {
       "value": "frost",
       "label": "Frost"
      },
      {
       "value": "dune",
       "label": "Dune"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "creatures",
     "title": "Night creatures",
     "type": "toggle",
     "default": true,
     "group": "Game"
    },
    {
     "key": "day_length",
     "title": "Day length (s)",
     "type": "integer",
     "default": 50,
     "min": 20,
     "max": 180,
     "group": "Game"
    },
    {
     "key": "hearts",
     "title": "Hearts",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "match_days",
     "title": "Match length (days)",
     "type": "integer",
     "description": "Co-op and Ore Rush end after this many days",
     "default": 2,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "rush",
       "label": "Ore Rush"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "meadow",
     "options": [
      {
       "value": "meadow",
       "label": "Meadow"
      },
      {
       "value": "caverns",
       "label": "Caverns"
      },
      {
       "value": "peaks",
       "label": "Peaks"
      },
      {
       "value": "lakes",
       "label": "Lakes"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "dpad",
     "gamepad",
     "joystick",
     "keyboard"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 2,
      "teams": "coop"
     },
     {
      "id": "rush",
      "name": "Ore Rush",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "meadow",
      "label": "Meadow"
     },
     {
      "id": "caverns",
      "label": "Caverns"
     },
     {
      "id": "peaks",
      "label": "Peaks"
     },
     {
      "id": "lakes",
      "label": "Lakes"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "overworld",
      "label": "Overworld"
     },
     {
      "value": "frost",
      "label": "Frost"
     },
     {
      "value": "dune",
      "label": "Dune"
     }
    ]
   }
  },
  {
   "id": "dino",
   "name": "Dino Runner",
   "category": "games",
   "icon": "footprints",
   "description": "An endless desert run: jump cacti, duck birds, day turns to night. Up/A jumps, down ducks. Race: up to four runners on one track, last one running wins.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/dino.html",
   "preview": "media/apps/dino.gif",
   "poster": "media/apps/dino.png",
   "og_image": "media/apps/og/dino.png",
   "source": "src/deskdot/apps/games_classic.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "desert",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "desert",
       "label": "Desert"
      },
      {
       "value": "tundra",
       "label": "Tundra"
      },
      {
       "value": "volcano",
       "label": "Volcano"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "birds",
     "title": "Birds",
     "type": "toggle",
     "default": true
    },
    {
     "key": "cycle",
     "title": "Day length (s)",
     "type": "integer",
     "default": 40,
     "min": 10,
     "max": 120
    },
    {
     "key": "start_speed",
     "title": "Start speed",
     "type": "integer",
     "default": 24,
     "min": 16,
     "max": 40,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "race",
       "label": "Race"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "desert",
     "options": [
      {
       "value": "desert",
       "label": "Desert"
      },
      {
       "value": "canyon",
       "label": "Canyon"
      },
      {
       "value": "rush",
       "label": "Rush"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "tap",
     "dpad",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "race",
      "name": "Race",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "desert",
      "label": "Desert"
     },
     {
      "id": "canyon",
      "label": "Canyon"
     },
     {
      "id": "rush",
      "label": "Rush"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "desert",
      "label": "Desert"
     },
     {
      "value": "tundra",
      "label": "Tundra"
     },
     {
      "value": "volcano",
      "label": "Volcano"
     }
    ]
   }
  },
  {
   "id": "flappy",
   "name": "Flappy",
   "category": "games",
   "icon": "bird",
   "description": "A little bird threading between pipes. Plays itself; up or A flaps. Race: up to four birds through the same pipes, last one flying wins.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/flappy.html",
   "preview": "media/apps/flappy.gif",
   "poster": "media/apps/flappy.png",
   "og_image": "media/apps/og/flappy.png",
   "source": "src/deskdot/apps/games_classic.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "meadow",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "meadow",
       "label": "Meadow"
      },
      {
       "value": "dusk",
       "label": "Dusk"
      },
      {
       "value": "abyss",
       "label": "Abyss"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "gap",
     "title": "Gap size",
     "type": "integer",
     "default": 11,
     "min": 8,
     "max": 15
    },
    {
     "key": "gravity",
     "title": "Gravity",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10,
     "group": "Game"
    },
    {
     "key": "shrink",
     "title": "Race: gaps shrink (sudden death)",
     "type": "toggle",
     "default": true,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "race",
       "label": "Race"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "pipes",
     "options": [
      {
       "value": "pipes",
       "label": "Pipes"
      },
      {
       "value": "moving",
       "label": "Moving"
      },
      {
       "value": "cave",
       "label": "Cave"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "tap",
     "dpad",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "race",
      "name": "Race",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "pipes",
      "label": "Pipes"
     },
     {
      "id": "moving",
      "label": "Moving"
     },
     {
      "id": "cave",
      "label": "Cave"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "meadow",
      "label": "Meadow"
     },
     {
      "value": "dusk",
      "label": "Dusk"
     },
     {
      "value": "abyss",
      "label": "Abyss"
     }
    ]
   }
  },
  {
   "id": "fourup",
   "name": "Four Up",
   "category": "games",
   "icon": "grid-3x3",
   "description": "Four-in-a-row. Two AIs play until someone takes a seat: left/right aim, A or down drops your disc. A phone can join as the second player. Variants: Pop Out (up pops your bottom disc) and Stones.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/fourup.html",
   "preview": "media/apps/fourup.gif",
   "poster": "media/apps/fourup.png",
   "og_image": "media/apps/og/fourup.png",
   "source": "src/deskdot/apps/games_connect.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "tavern",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "tavern",
       "label": "Tavern"
      },
      {
       "value": "ice",
       "label": "Ice"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "first_to",
     "title": "Match: first to",
     "type": "integer",
     "description": "Round wins needed to take a match started from the menu",
     "default": 3,
     "min": 1,
     "max": 9,
     "group": "Game"
    },
    {
     "key": "stones",
     "title": "Stones (Stones map)",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Vs AI"
      },
      {
       "value": "duel",
       "label": "Vs Friend"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "popout",
       "label": "Pop Out"
      },
      {
       "value": "stones",
       "label": "Stones"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 2,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 2,
    "controls": [
     "dpad",
     "tap",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Vs AI",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "duel",
      "name": "Vs Friend",
      "min_players": 2,
      "max_players": 2,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Classic"
     },
     {
      "id": "popout",
      "label": "Pop Out"
     },
     {
      "id": "stones",
      "label": "Stones"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "tavern",
      "label": "Tavern"
     },
     {
      "value": "ice",
      "label": "Ice"
     }
    ]
   }
  },
  {
   "id": "gamedeals",
   "name": "Game Deals",
   "category": "games",
   "icon": "gamepad-2",
   "description": "Free-game giveaways and the best PC game discounts, with store tags and countdowns.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/gamedeals.html",
   "preview": "media/apps/gamedeals.gif",
   "poster": "media/apps/gamedeals.png",
   "og_image": "media/apps/og/gamedeals.png",
   "source": "src/deskdot/apps/gamedeals.py",
   "actions": [
    {
     "id": "next",
     "label": "Next game"
    },
    {
     "id": "refresh",
     "label": "Refresh"
    }
   ],
   "settings": [
    {
     "key": "source",
     "title": "Show",
     "type": "choice",
     "default": "both",
     "options": [
      {
       "value": "giveaways",
       "label": "Free giveaways"
      },
      {
       "value": "deals",
       "label": "Discounts"
      },
      {
       "value": "both",
       "label": "Both"
      }
     ]
    },
    {
     "key": "platform",
     "title": "Giveaways: platform",
     "type": "choice",
     "default": "pc",
     "options": [
      {
       "value": "pc",
       "label": "PC (all stores)"
      },
      {
       "value": "steam",
       "label": "Steam"
      },
      {
       "value": "epic-games-store",
       "label": "Epic Games Store"
      },
      {
       "value": "gog",
       "label": "GOG"
      },
      {
       "value": "itchio",
       "label": "itch.io"
      },
      {
       "value": "ubisoft",
       "label": "Ubisoft"
      },
      {
       "value": "ps4",
       "label": "PlayStation"
      },
      {
       "value": "xbox-one",
       "label": "Xbox"
      },
      {
       "value": "switch",
       "label": "Switch"
      },
      {
       "value": "android",
       "label": "Android"
      },
      {
       "value": "ios",
       "label": "iOS"
      }
     ],
     "group": "Filters"
    },
    {
     "key": "sort",
     "title": "Giveaways: order",
     "type": "choice",
     "default": "popularity",
     "options": [
      {
       "value": "popularity",
       "label": "Most popular"
      },
      {
       "value": "date",
       "label": "Newest"
      },
      {
       "value": "value",
       "label": "Highest value"
      }
     ],
     "group": "Filters"
    },
    {
     "key": "store",
     "title": "Discounts: store",
     "type": "choice",
     "default": "any",
     "options": [
      {
       "value": "any",
       "label": "Any store"
      },
      {
       "value": "1",
       "label": "Steam"
      },
      {
       "value": "25",
       "label": "Epic Games Store"
      },
      {
       "value": "7",
       "label": "GOG"
      },
      {
       "value": "11",
       "label": "Humble Store"
      },
      {
       "value": "15",
       "label": "Fanatical"
      },
      {
       "value": "3",
       "label": "GreenManGaming"
      },
      {
       "value": "27",
       "label": "Gamesplanet"
      },
      {
       "value": "23",
       "label": "GameBillet"
      }
     ],
     "group": "Filters"
    },
    {
     "key": "max_price",
     "title": "Discounts: max price ($)",
     "type": "number",
     "description": "0 = any",
     "default": 0,
     "min": 0,
     "max": 100,
     "group": "Filters"
    },
    {
     "key": "min_savings",
     "title": "Discounts: min savings %",
     "type": "integer",
     "default": 50,
     "min": 0,
     "max": 99,
     "group": "Filters"
    },
    {
     "key": "count",
     "title": "Games in rotation",
     "type": "integer",
     "default": 10,
     "min": 1,
     "max": 30,
     "group": "Filters"
    },
    {
     "key": "rotate",
     "title": "Seconds per game",
     "type": "integer",
     "default": 10,
     "min": 4,
     "max": 120,
     "group": "Display"
    },
    {
     "key": "notify",
     "title": "Notify on new free games",
     "type": "toggle",
     "default": true,
     "group": "Display"
    }
   ],
   "game": {
    "max_players": 1,
    "controls": [],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "infinity",
   "name": "Infinity",
   "category": "games",
   "icon": "infinity",
   "description": "A tiny ship flying forever through a procedurally generated neon cave. Up/down to steer.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/infinity.html",
   "preview": "media/apps/infinity.gif",
   "poster": "media/apps/infinity.png",
   "og_image": "media/apps/og/infinity.png",
   "source": "src/deskdot/apps/games_action.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "lava",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "lava",
       "label": "Lava Cave"
      },
      {
       "value": "ice",
       "label": "Ice Cave"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "trail",
     "title": "Rainbow trail",
     "type": "toggle",
     "default": true,
     "group": "Graphics"
    },
    {
     "key": "stars",
     "title": "Star field",
     "type": "toggle",
     "default": true,
     "group": "Graphics"
    },
    {
     "key": "spikes",
     "title": "Stalactites",
     "type": "toggle",
     "default": true,
     "group": "Game"
    },
    {
     "key": "accel",
     "title": "Speed-up",
     "type": "number",
     "description": "How fast the flight accelerates",
     "default": 0.25,
     "min": 0.0,
     "max": 1.0,
     "group": "Game"
    },
    {
     "key": "lives",
     "title": "Convoy respawns",
     "type": "integer",
     "description": "Shared respawns in Convoy mode",
     "default": 3,
     "min": 0,
     "max": 9,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "survive",
       "label": "Survival"
      },
      {
       "value": "coop",
       "label": "Convoy"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "cave",
     "options": [
      {
       "value": "cave",
       "label": "Cave"
      },
      {
       "value": "tunnel",
       "label": "Tunnel"
      },
      {
       "value": "canyon",
       "label": "Canyon"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "tap",
     "dpad",
     "tilt",
     "joystick"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "survive",
      "name": "Survival",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "coop",
      "name": "Convoy",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     }
    ],
    "maps": [
     {
      "id": "cave",
      "label": "Cave"
     },
     {
      "id": "tunnel",
      "label": "Tunnel"
     },
     {
      "id": "canyon",
      "label": "Canyon"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "lava",
      "label": "Lava Cave"
     },
     {
      "value": "ice",
      "label": "Ice Cave"
     }
    ]
   }
  },
  {
   "id": "invaders",
   "name": "Invaders",
   "category": "games",
   "icon": "rocket",
   "description": "Hold the line against a marching alien fleet behind crumbling shields. ←/→ move, A fires.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/invaders.html",
   "preview": "media/apps/invaders.gif",
   "poster": "media/apps/invaders.png",
   "og_image": "media/apps/og/invaders.png",
   "source": "src/deskdot/apps/games_action.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "deep",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "deep",
       "label": "Deep Space"
      },
      {
       "value": "sunset",
       "label": "Sunset"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "rows",
     "title": "Alien rows",
     "type": "integer",
     "default": 3,
     "min": 2,
     "max": 4,
     "group": "Game"
    },
    {
     "key": "shields",
     "title": "Shields",
     "type": "toggle",
     "default": true,
     "group": "Game"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "fire",
     "title": "Alien fire",
     "type": "integer",
     "description": "How often the fleet shoots back",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "duel_waves",
     "title": "Duel waves",
     "type": "integer",
     "description": "Waves in a Duel match",
     "default": 2,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "duel",
       "label": "Duel"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "bunkers",
       "label": "Bunkers"
      },
      {
       "value": "wall",
       "label": "Long Wall"
      },
      {
       "value": "open",
       "label": "Open Sky"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "dpad",
     "joystick",
     "gamepad",
     "keyboard"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     },
     {
      "id": "duel",
      "name": "Duel",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Classic"
     },
     {
      "id": "bunkers",
      "label": "Bunkers"
     },
     {
      "id": "wall",
      "label": "Long Wall"
     },
     {
      "id": "open",
      "label": "Open Sky"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "deep",
      "label": "Deep Space"
     },
     {
      "value": "sunset",
      "label": "Sunset"
     }
    ]
   }
  },
  {
   "id": "leafleap",
   "name": "Leaf Leap",
   "category": "games",
   "icon": "sprout",
   "description": "A flip-screen platformer: Sprig runs, jumps pits, stomps beetles and grabs coins on the way to the flag. Left/right run, up/A jumps. Two sprouts play co-op (shared lives) or race to the flag.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/leafleap.html",
   "preview": "media/apps/leafleap.gif",
   "poster": "media/apps/leafleap.png",
   "og_image": "media/apps/og/leafleap.png",
   "source": "src/deskdot/apps/games_platform.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "meadow",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "meadow",
       "label": "Meadow"
      },
      {
       "value": "autumn",
       "label": "Autumn"
      },
      {
       "value": "crystal",
       "label": "Crystal cave"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "enemies",
     "title": "Beetles",
     "type": "toggle",
     "default": true,
     "group": "Game"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "race_levels",
     "title": "Race length (levels)",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "race",
       "label": "Race"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "meadow",
     "options": [
      {
       "value": "meadow",
       "label": "Meadow"
      },
      {
       "value": "caves",
       "label": "Caves"
      },
      {
       "value": "sky",
       "label": "Sky Isles"
      },
      {
       "value": "castle",
       "label": "Castle"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 2,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 2,
    "controls": [
     "dpad",
     "gamepad",
     "joystick",
     "keyboard"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 2,
      "teams": "coop"
     },
     {
      "id": "race",
      "name": "Race",
      "min_players": 2,
      "max_players": 2,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "meadow",
      "label": "Meadow"
     },
     {
      "id": "caves",
      "label": "Caves"
     },
     {
      "id": "sky",
      "label": "Sky Isles"
     },
     {
      "id": "castle",
      "label": "Castle"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "meadow",
      "label": "Meadow"
     },
     {
      "value": "autumn",
      "label": "Autumn"
     },
     {
      "value": "crystal",
      "label": "Crystal cave"
     }
    ]
   }
  },
  {
   "id": "cycles",
   "name": "Light Cycles",
   "category": "games",
   "icon": "zap",
   "description": "Up to four glowing bikes, walls behind them — box the others in, don't crash. Plays itself; arrows steer the blue bike, and up to three friends can join from their phones on your Wi-Fi. Party, 2v2 teams and four arenas.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/cycles.html",
   "preview": "media/apps/cycles.gif",
   "poster": "media/apps/cycles.png",
   "og_image": "media/apps/og/cycles.png",
   "source": "src/deskdot/apps/games_party.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "grid",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "grid",
       "label": "Grid"
      },
      {
       "value": "sunset",
       "label": "Sunset"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "riders",
     "title": "Bikes in the arena",
     "type": "integer",
     "description": "Empty seats are ridden by the AI",
     "default": 4,
     "min": 2,
     "max": 4
    },
    {
     "key": "pace",
     "title": "Steps per second",
     "type": "number",
     "default": 9.0,
     "min": 5.0,
     "max": 14.0
    },
    {
     "key": "first_to",
     "title": "Match: first to",
     "type": "integer",
     "description": "Round wins needed to take the match",
     "default": 5,
     "min": 1,
     "max": 9,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "party",
       "label": "Party"
      },
      {
       "value": "teams",
       "label": "Teams"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "open",
     "options": [
      {
       "value": "open",
       "label": "Open"
      },
      {
       "value": "pillars",
       "label": "Pillars"
      },
      {
       "value": "core",
       "label": "Core"
      },
      {
       "value": "wrap",
       "label": "Wrap"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "swipe",
     "dpad",
     "joystick",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "party",
      "name": "Party",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "teams",
      "name": "Teams",
      "min_players": 2,
      "max_players": 4,
      "teams": "versus"
     }
    ],
    "maps": [
     {
      "id": "open",
      "label": "Open"
     },
     {
      "id": "pillars",
      "label": "Pillars"
     },
     {
      "id": "core",
      "label": "Core"
     },
     {
      "id": "wrap",
      "label": "Wrap"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "grid",
      "label": "Grid"
     },
     {
      "value": "sunset",
      "label": "Sunset"
     }
    ]
   }
  },
  {
   "id": "maze",
   "name": "Maze Chase",
   "category": "games",
   "icon": "ghost",
   "description": "Eat every dot in a fresh wrap-around maze while ghosts give chase; power pills turn the tables.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/maze.html",
   "preview": "media/apps/maze.gif",
   "poster": "media/apps/maze.png",
   "og_image": "media/apps/og/maze.png",
   "source": "src/deskdot/apps/games_action.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "haunted",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "haunted",
       "label": "Haunted"
      },
      {
       "value": "hedge",
       "label": "Hedge Maze"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "ghosts",
     "title": "Ghosts",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 4,
     "group": "Game"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "hunt_time",
     "title": "Hunt round (s)",
     "type": "integer",
     "description": "Seconds the runner must survive in Hunt mode",
     "default": 40,
     "min": 15,
     "max": 90,
     "group": "Game"
    },
    {
     "key": "fright",
     "title": "Power pill (s)",
     "type": "number",
     "default": 6.0,
     "min": 2.0,
     "max": 10.0,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "hunt",
       "label": "Hunt"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "random",
     "options": [
      {
       "value": "random",
       "label": "Random"
      },
      {
       "value": "mirror",
       "label": "Mirror"
      },
      {
       "value": "open",
       "label": "Open"
      },
      {
       "value": "boxed",
       "label": "Boxed"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "dpad",
     "swipe",
     "joystick",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 2,
      "teams": "coop"
     },
     {
      "id": "hunt",
      "name": "Hunt",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "random",
      "label": "Random"
     },
     {
      "id": "mirror",
      "label": "Mirror"
     },
     {
      "id": "open",
      "label": "Open"
     },
     {
      "id": "boxed",
      "label": "Boxed"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "haunted",
      "label": "Haunted"
     },
     {
      "value": "hedge",
      "label": "Hedge Maze"
     }
    ]
   }
  },
  {
   "id": "mines",
   "name": "Mines",
   "category": "games",
   "icon": "bomb",
   "description": "Minesweeper that solves itself with logic (and the odd guess). Arrows move the cursor, A reveals, B flags. Up to four players sweep one field together (co-op) or race for cells.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/mines.html",
   "preview": "media/apps/mines.gif",
   "poster": "media/apps/mines.png",
   "og_image": "media/apps/og/mines.png",
   "source": "src/deskdot/apps/games_bonus.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "field",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "field",
       "label": "Field"
      },
      {
       "value": "frost",
       "label": "Frost"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "mines",
     "title": "Mines",
     "type": "integer",
     "default": 14,
     "min": 6,
     "max": 25
    },
    {
     "key": "lives",
     "title": "Co-op lives",
     "type": "integer",
     "description": "Mines the team can survive in co-op",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "stun",
     "title": "Race: stun after a mine (s)",
     "type": "number",
     "default": 2.5,
     "min": 1.0,
     "max": 6.0,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Co-op"
      },
      {
       "value": "race",
       "label": "Race"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "square",
     "options": [
      {
       "value": "square",
       "label": "Square"
      },
      {
       "value": "donut",
       "label": "Donut"
      },
      {
       "value": "cross",
       "label": "Cross"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "dpad",
     "gamepad",
     "keyboard"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     },
     {
      "id": "race",
      "name": "Race",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "square",
      "label": "Square"
     },
     {
      "id": "donut",
      "label": "Donut"
     },
     {
      "id": "cross",
      "label": "Cross"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "field",
      "label": "Field"
     },
     {
      "value": "frost",
      "label": "Frost"
     }
    ]
   }
  },
  {
   "id": "neonheat",
   "name": "Neon Heat",
   "category": "games",
   "icon": "siren",
   "description": "Top-down neon city getaway: grab packages, deliver them and shake the police. Arrows turn at the next junction, the opposite arrow U-turns. Up to four couriers race for the package and steal it by ramming, or play Cops & Robbers.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/neonheat.html",
   "preview": "media/apps/neonheat.gif",
   "poster": "media/apps/neonheat.png",
   "og_image": "media/apps/og/neonheat.png",
   "source": "src/deskdot/apps/games_city.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "synthwave",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "synthwave",
       "label": "Synthwave"
      },
      {
       "value": "noir",
       "label": "Noir"
      },
      {
       "value": "toxic",
       "label": "Toxic"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "traffic",
     "title": "Traffic",
     "type": "integer",
     "default": 5,
     "min": 0,
     "max": 10,
     "group": "Game"
    },
    {
     "key": "police",
     "title": "Police cars",
     "type": "integer",
     "description": "How many chase you with a package",
     "default": 2,
     "min": 1,
     "max": 4,
     "group": "Game"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "match_time",
     "title": "Match length (s)",
     "type": "integer",
     "description": "Package Rush and Cops & Robbers end when the clock runs out",
     "default": 150,
     "min": 60,
     "max": 300,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "rush",
       "label": "Package Rush"
      },
      {
       "value": "cops",
       "label": "Cops & Robbers"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "downtown",
     "options": [
      {
       "value": "downtown",
       "label": "Downtown"
      },
      {
       "value": "oldtown",
       "label": "Old Town"
      },
      {
       "value": "suburbs",
       "label": "Suburbs"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "swipe",
     "dpad",
     "joystick",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "rush",
      "name": "Package Rush",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "cops",
      "name": "Cops & Robbers",
      "min_players": 2,
      "max_players": 4,
      "teams": "versus"
     }
    ],
    "maps": [
     {
      "id": "downtown",
      "label": "Downtown"
     },
     {
      "id": "oldtown",
      "label": "Old Town"
     },
     {
      "id": "suburbs",
      "label": "Suburbs"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "synthwave",
      "label": "Synthwave"
     },
     {
      "value": "noir",
      "label": "Noir"
     },
     {
      "value": "toxic",
      "label": "Toxic"
     }
    ]
   }
  },
  {
   "id": "pong",
   "name": "Pong",
   "category": "games",
   "icon": "circle-dot",
   "description": "Two paddles, one ball that speeds up every hit. Plays itself; up/down steers the left paddle. Modes: 1 v 1, Doubles (2 v 2, front and back paddles) and 4 Way (a paddle on every wall). Friends join from their phones (local Wi-Fi).",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/pong.html",
   "preview": "media/apps/pong.gif",
   "poster": "media/apps/pong.png",
   "og_image": "media/apps/og/pong.png",
   "source": "src/deskdot/apps/games_classic.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "arcade",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "arcade",
       "label": "Arcade"
      },
      {
       "value": "court",
       "label": "Court"
      },
      {
       "value": "ice",
       "label": "Ice"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "paddle",
     "title": "Paddle size",
     "type": "integer",
     "default": 7,
     "min": 4,
     "max": 12
    },
    {
     "key": "points",
     "title": "Points to win",
     "type": "integer",
     "default": 7,
     "min": 3,
     "max": 21
    },
    {
     "key": "lives",
     "title": "Lives (4 Way)",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 9,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "1 v 1"
      },
      {
       "value": "doubles",
       "label": "Doubles"
      },
      {
       "value": "fourway",
       "label": "4 Way"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "open",
     "options": [
      {
       "value": "open",
       "label": "Open"
      },
      {
       "value": "block",
       "label": "Block"
      },
      {
       "value": "portals",
       "label": "Portals"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "joystick",
     "dpad",
     "gamepad"
    ],
    "modes": [
     {
      "id": "classic",
      "name": "1 v 1",
      "min_players": 1,
      "max_players": 2,
      "teams": "versus"
     },
     {
      "id": "doubles",
      "name": "Doubles",
      "min_players": 4,
      "max_players": 4,
      "teams": "versus"
     },
     {
      "id": "fourway",
      "name": "4 Way",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "open",
      "label": "Open"
     },
     {
      "id": "block",
      "label": "Block"
     },
     {
      "id": "portals",
      "label": "Portals"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "arcade",
      "label": "Arcade"
     },
     {
      "value": "court",
      "label": "Court"
     },
     {
      "value": "ice",
      "label": "Ice"
     }
    ]
   }
  },
  {
   "id": "racer",
   "name": "Racer",
   "category": "games",
   "icon": "car",
   "description": "Top-down three-lane highway dash through traffic. Left/right changes lanes. Race: 2-4 cars on one road, last car running wins.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/racer.html",
   "preview": "media/apps/racer.gif",
   "poster": "media/apps/racer.png",
   "og_image": "media/apps/og/racer.png",
   "source": "src/deskdot/apps/games_classic.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "asphalt",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "asphalt",
       "label": "Asphalt"
      },
      {
       "value": "midnight",
       "label": "Midnight"
      },
      {
       "value": "dunes",
       "label": "Dunes"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "traffic",
     "title": "Traffic",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "race",
       "label": "Race"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "highway",
     "options": [
      {
       "value": "highway",
       "label": "Highway"
      },
      {
       "value": "roadworks",
       "label": "Roadworks"
      },
      {
       "value": "rush",
       "label": "Rush hour"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "tilt",
     "swipe",
     "dpad",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "race",
      "name": "Race",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "highway",
      "label": "Highway"
     },
     {
      "id": "roadworks",
      "label": "Roadworks"
     },
     {
      "id": "rush",
      "label": "Rush hour"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "asphalt",
      "label": "Asphalt"
     },
     {
      "value": "midnight",
      "label": "Midnight"
     },
     {
      "value": "dunes",
      "label": "Dunes"
     }
    ]
   }
  },
  {
   "id": "rps",
   "name": "Rock Paper Scissors",
   "category": "games",
   "icon": "hand",
   "description": "The party classic, 1 v 1: two pixel hands pump ROCK… PAPER… SCISSORS… SHOOT! Play the AI, a friend (keyboard + phone, or two phones) or run a 3–8 player tournament — friends join by scanning the panel's QR code and pick their hand on big buttons. Plays AI vs AI when idle.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/rps.html",
   "preview": "media/apps/rps.gif",
   "poster": "media/apps/rps.png",
   "og_image": "media/apps/og/rps.png",
   "source": "src/deskdot/apps/games_rps.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "arena",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "arena",
       "label": "Arena"
      },
      {
       "value": "party",
       "label": "Party"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "best_of",
     "title": "Match length",
     "type": "choice",
     "default": "3",
     "options": [
      {
       "value": "1",
       "label": "Best of 1"
      },
      {
       "value": "3",
       "label": "Best of 3"
      },
      {
       "value": "5",
       "label": "Best of 5"
      },
      {
       "value": "7",
       "label": "Best of 7"
      }
     ],
     "group": "Game"
    },
    {
     "key": "round_timer",
     "title": "Seconds to pick",
     "type": "integer",
     "description": "Anyone who hasn't picked when time runs out gets a random hand",
     "default": 8,
     "min": 3,
     "max": 30,
     "group": "Game"
    },
    {
     "key": "ai_style",
     "title": "AI style",
     "type": "choice",
     "default": "random",
     "options": [
      {
       "value": "random",
       "label": "Random (truly uniform — unbeatable in the long run)"
      },
      {
       "value": "pattern",
       "label": "Pattern hunter (punishes your habits)"
      },
      {
       "value": "cheeky",
       "label": "Cheeky (gives away tells… mostly honest)"
      }
     ],
     "group": "Game"
    },
    {
     "key": "reveal",
     "title": "Reveal",
     "type": "choice",
     "default": "shoot",
     "options": [
      {
       "value": "shoot",
       "label": "Rock, paper, scissors, shoot!"
      },
      {
       "value": "count",
       "label": "3-2-1 countdown"
      },
      {
       "value": "instant",
       "label": "Instant"
      }
     ],
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "cpu",
     "options": [
      {
       "value": "cpu",
       "label": "You vs AI"
      },
      {
       "value": "pvp",
       "label": "1 v 1"
      },
      {
       "value": "tour",
       "label": "Tournament"
      },
      {
       "value": "demo",
       "label": "AI vs AI"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "knockout",
     "options": [
      {
       "value": "knockout",
       "label": "Knockout"
      },
      {
       "value": "robin",
       "label": "Round robin"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 8,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 8,
    "controls": [
     "rps",
     "dpad",
     "gamepad"
    ],
    "modes": [
     {
      "id": "cpu",
      "name": "You vs AI",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "pvp",
      "name": "1 v 1",
      "min_players": 2,
      "max_players": 2,
      "teams": "ffa"
     },
     {
      "id": "tour",
      "name": "Tournament",
      "min_players": 3,
      "max_players": 8,
      "teams": "ffa"
     },
     {
      "id": "demo",
      "name": "AI vs AI",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     }
    ],
    "maps": [
     {
      "id": "knockout",
      "label": "Knockout"
     },
     {
      "id": "robin",
      "label": "Round robin"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "arena",
      "label": "Arena"
     },
     {
      "value": "party",
      "label": "Party"
     }
    ]
   }
  },
  {
   "id": "arcade",
   "name": "Snake",
   "category": "games",
   "icon": "gamepad-2",
   "description": "Snake on the LEDs. Plays itself as a screensaver; press the arrow keys to play. B opens the menu: Battle and Teams for 2–4 snakes, Co-op with shared apples, and four arenas.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/arcade.html",
   "preview": "media/apps/arcade.gif",
   "poster": "media/apps/arcade.png",
   "og_image": "media/apps/og/arcade.png",
   "source": "src/deskdot/apps/arcade.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed (moves/s)",
     "type": "integer",
     "default": 8,
     "min": 3,
     "max": 15
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "night",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "night",
       "label": "Night"
      },
      {
       "value": "jungle",
       "label": "Jungle"
      },
      {
       "value": "circuit",
       "label": "Circuit"
      },
      {
       "value": "desert",
       "label": "Desert"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "color",
     "title": "Snake colour",
     "type": "color",
     "default": "#00ff8c"
    },
    {
     "key": "food",
     "title": "Food colour",
     "type": "color",
     "default": "#ff1e5a"
    },
    {
     "key": "walls",
     "title": "Solid walls",
     "type": "toggle",
     "description": "On the open map the edges kill instead of wrapping around",
     "default": false
    },
    {
     "key": "grow",
     "title": "Growth per apple",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 3,
     "group": "Game"
    },
    {
     "key": "match_time",
     "title": "Match time (s)",
     "type": "integer",
     "description": "Battle and Teams",
     "default": 90,
     "min": 30,
     "max": 300,
     "group": "Game"
    },
    {
     "key": "lives",
     "title": "Co-op lives",
     "type": "integer",
     "default": 3,
     "min": 0,
     "max": 9,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "battle",
       "label": "Battle"
      },
      {
       "value": "teams",
       "label": "Teams"
      },
      {
       "value": "coop",
       "label": "Co-op"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Open"
      },
      {
       "value": "pillars",
       "label": "Pillars"
      },
      {
       "value": "maze",
       "label": "Maze"
      },
      {
       "value": "tunnels",
       "label": "Tunnels"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "swipe",
     "dpad",
     "joystick",
     "gamepad",
     "keyboard"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "battle",
      "name": "Battle",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "teams",
      "name": "Teams",
      "min_players": 2,
      "max_players": 4,
      "teams": "versus"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Open"
     },
     {
      "id": "pillars",
      "label": "Pillars"
     },
     {
      "id": "maze",
      "label": "Maze"
     },
     {
      "id": "tunnels",
      "label": "Tunnels"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "night",
      "label": "Night"
     },
     {
      "value": "jungle",
      "label": "Jungle"
     },
     {
      "value": "circuit",
      "label": "Circuit"
     },
     {
      "value": "desert",
      "label": "Desert"
     }
    ]
   }
  },
  {
   "id": "starship",
   "name": "Starship",
   "category": "games",
   "icon": "plane",
   "description": "A vertical space shooter: weave through swooping enemy waves. ←/→ move, auto-fire, A bomb. Fly with up to three wingmen (co-op, a downed ship is revived when the wave is cleared) or dogfight for points (Ace). Arenas: deep space, an asteroid belt and a narrow canyon.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/starship.html",
   "preview": "media/apps/starship.gif",
   "poster": "media/apps/starship.png",
   "og_image": "media/apps/og/starship.png",
   "source": "src/deskdot/apps/games_bonus.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "void",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "void",
       "label": "Void"
      },
      {
       "value": "solar",
       "label": "Solar"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "lives",
     "title": "Lives",
     "type": "integer",
     "default": 3,
     "min": 1,
     "max": 5
    },
    {
     "key": "ace_time",
     "title": "Ace duel length (s)",
     "type": "integer",
     "description": "Dogfight mode: most points when the clock runs out wins",
     "default": 90,
     "min": 30,
     "max": 240,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "coop",
       "label": "Wingmen"
      },
      {
       "value": "ace",
       "label": "Ace"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "deep",
     "options": [
      {
       "value": "deep",
       "label": "Deep space"
      },
      {
       "value": "rocks",
       "label": "Asteroids"
      },
      {
       "value": "canyon",
       "label": "Canyon"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "joystick",
     "dpad",
     "tilt",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "coop",
      "name": "Wingmen",
      "min_players": 2,
      "max_players": 4,
      "teams": "coop"
     },
     {
      "id": "ace",
      "name": "Ace",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "deep",
      "label": "Deep space"
     },
     {
      "id": "rocks",
      "label": "Asteroids"
     },
     {
      "id": "canyon",
      "label": "Canyon"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "void",
      "label": "Void"
     },
     {
      "value": "solar",
      "label": "Solar"
     }
    ]
   }
  },
  {
   "id": "streetsurge",
   "name": "Street Surge",
   "category": "games",
   "icon": "gauge",
   "description": "Pseudo-3D night street race: laps of curves and hills against rival cars on four tracks. Left/right steer, A fires nitro, down/B brakes. Up to four players race each other or in two teams; the camera follows whoever is behind.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/streetsurge.html",
   "preview": "media/apps/streetsurge.gif",
   "poster": "media/apps/streetsurge.png",
   "og_image": "media/apps/og/streetsurge.png",
   "source": "src/deskdot/apps/games_road.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "midnight",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "midnight",
       "label": "Midnight"
      },
      {
       "value": "dusk",
       "label": "Dusk"
      },
      {
       "value": "frost",
       "label": "Frost"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "rivals",
     "title": "Rivals",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 7,
     "group": "Game"
    },
    {
     "key": "hills",
     "title": "Hills",
     "type": "toggle",
     "default": true,
     "group": "Game"
    },
    {
     "key": "laps",
     "title": "Laps",
     "type": "integer",
     "default": 2,
     "min": 1,
     "max": 5,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Grand Prix"
      },
      {
       "value": "race",
       "label": "Versus"
      },
      {
       "value": "teams",
       "label": "Team Race"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "city",
     "options": [
      {
       "value": "city",
       "label": "City"
      },
      {
       "value": "coast",
       "label": "Coast Road"
      },
      {
       "value": "pass",
       "label": "Mountain Pass"
      },
      {
       "value": "speedway",
       "label": "Speedway"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 4,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 4,
    "controls": [
     "tap",
     "tilt",
     "joystick",
     "gamepad",
     "dpad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Grand Prix",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "race",
      "name": "Versus",
      "min_players": 2,
      "max_players": 4,
      "teams": "ffa"
     },
     {
      "id": "teams",
      "name": "Team Race",
      "min_players": 2,
      "max_players": 4,
      "teams": "versus"
     }
    ],
    "maps": [
     {
      "id": "city",
      "label": "City"
     },
     {
      "id": "coast",
      "label": "Coast Road"
     },
     {
      "id": "pass",
      "label": "Mountain Pass"
     },
     {
      "id": "speedway",
      "label": "Speedway"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "midnight",
      "label": "Midnight"
     },
     {
      "value": "dusk",
      "label": "Dusk"
     },
     {
      "value": "frost",
      "label": "Frost"
     }
    ]
   }
  },
  {
   "id": "tetris",
   "name": "Tetris",
   "category": "games",
   "icon": "blocks",
   "description": "Falling blocks with a next-piece preview. The AI stacks by holes, height and bumpiness. ←/→ move, ↑ rotate, ↓ soft drop, A hard drop, B rotate back. B in the demo opens the menu: Versus (clear lines to send garbage) and Co-op (one wide well).",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/tetris.html",
   "preview": "media/apps/tetris.gif",
   "poster": "media/apps/tetris.png",
   "og_image": "media/apps/og/tetris.png",
   "source": "src/deskdot/apps/games_puzzle.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "prism",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "prism",
       "label": "Prism"
      },
      {
       "value": "candy",
       "label": "Candy"
      },
      {
       "value": "deep",
       "label": "Deep sea"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "ghost",
     "title": "Landing shadow",
     "type": "toggle",
     "default": true
    },
    {
     "key": "start_level",
     "title": "Start level",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 10
    },
    {
     "key": "attack",
     "title": "Garbage sent",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic (2 lines → 1)"
      },
      {
       "value": "mirror",
       "label": "Mirror (every line)"
      }
     ],
     "group": "Game"
    },
    {
     "key": "rise_every",
     "title": "Rising floor every (s)",
     "type": "integer",
     "default": 15,
     "min": 5,
     "max": 60,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Solo"
      },
      {
       "value": "versus",
       "label": "Versus"
      },
      {
       "value": "coop",
       "label": "Co-op"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Empty"
      },
      {
       "value": "mound",
       "label": "Mound"
      },
      {
       "value": "rising",
       "label": "Rising"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 2,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 2,
    "controls": [
     "dpad",
     "gamepad",
     "keyboard",
     "joystick"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Solo",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "versus",
      "name": "Versus",
      "min_players": 2,
      "max_players": 2,
      "teams": "versus"
     },
     {
      "id": "coop",
      "name": "Co-op",
      "min_players": 2,
      "max_players": 2,
      "teams": "coop"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Empty"
     },
     {
      "id": "mound",
      "label": "Mound"
     },
     {
      "id": "rising",
      "label": "Rising"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "prism",
      "label": "Prism"
     },
     {
      "value": "candy",
      "label": "Candy"
     },
     {
      "value": "deep",
      "label": "Deep sea"
     }
    ]
   }
  },
  {
   "id": "trivia",
   "name": "Trivia",
   "category": "games",
   "icon": "circle-question-mark",
   "description": "Open Trivia DB quiz: question, options A–D, countdown ring, reveal. Play with the arrow keys.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/trivia.html",
   "preview": "media/apps/trivia.gif",
   "poster": "media/apps/trivia.png",
   "og_image": "media/apps/og/trivia.png",
   "source": "src/deskdot/apps/trivia.py",
   "actions": [
    {
     "id": "next",
     "label": "Next question"
    },
    {
     "id": "reveal",
     "label": "Reveal"
    },
    {
     "id": "reset_score",
     "label": "Reset score"
    }
   ],
   "settings": [
    {
     "key": "category",
     "title": "Category",
     "type": "choice",
     "default": "any",
     "options": [
      {
       "value": "any",
       "label": "Any category"
      },
      {
       "value": "9",
       "label": "General knowledge"
      },
      {
       "value": "10",
       "label": "Books"
      },
      {
       "value": "11",
       "label": "Film"
      },
      {
       "value": "12",
       "label": "Music"
      },
      {
       "value": "13",
       "label": "Musicals & theatre"
      },
      {
       "value": "14",
       "label": "Television"
      },
      {
       "value": "15",
       "label": "Video games"
      },
      {
       "value": "16",
       "label": "Board games"
      },
      {
       "value": "17",
       "label": "Science & nature"
      },
      {
       "value": "18",
       "label": "Computers"
      },
      {
       "value": "19",
       "label": "Mathematics"
      },
      {
       "value": "20",
       "label": "Mythology"
      },
      {
       "value": "21",
       "label": "Sports"
      },
      {
       "value": "22",
       "label": "Geography"
      },
      {
       "value": "23",
       "label": "History"
      },
      {
       "value": "24",
       "label": "Politics"
      },
      {
       "value": "25",
       "label": "Art"
      },
      {
       "value": "26",
       "label": "Celebrities"
      },
      {
       "value": "27",
       "label": "Animals"
      },
      {
       "value": "28",
       "label": "Vehicles"
      },
      {
       "value": "29",
       "label": "Comics"
      },
      {
       "value": "30",
       "label": "Gadgets"
      },
      {
       "value": "31",
       "label": "Anime & manga"
      },
      {
       "value": "32",
       "label": "Cartoons"
      }
     ],
     "group": "Questions"
    },
    {
     "key": "difficulty",
     "title": "Difficulty",
     "type": "choice",
     "default": "any",
     "options": [
      {
       "value": "any",
       "label": "Any"
      },
      {
       "value": "easy",
       "label": "Easy"
      },
      {
       "value": "medium",
       "label": "Medium"
      },
      {
       "value": "hard",
       "label": "Hard"
      }
     ],
     "group": "Questions"
    },
    {
     "key": "qtype",
     "title": "Type",
     "type": "choice",
     "default": "multiple",
     "options": [
      {
       "value": "multiple",
       "label": "Multiple choice"
      },
      {
       "value": "boolean",
       "label": "True / false"
      },
      {
       "value": "any",
       "label": "Mixed"
      }
     ],
     "group": "Questions"
    },
    {
     "key": "seconds",
     "title": "Seconds to answer",
     "type": "integer",
     "default": 15,
     "min": 5,
     "max": 60,
     "group": "Play"
    },
    {
     "key": "reveal_seconds",
     "title": "Reveal seconds",
     "type": "integer",
     "default": 4,
     "min": 2,
     "max": 15,
     "group": "Play"
    },
    {
     "key": "preview",
     "title": "Read out options",
     "type": "toggle",
     "description": "Show each option on its own card before the answer board",
     "default": true,
     "group": "Play"
    },
    {
     "key": "auto_play",
     "title": "Auto-play",
     "type": "toggle",
     "description": "Move on after the reveal by itself; off = wait for A / Next",
     "default": true,
     "group": "Play"
    },
    {
     "key": "pace",
     "title": "Reading pace",
     "type": "choice",
     "default": "normal",
     "options": [
      {
       "value": "slow",
       "label": "Slow"
      },
      {
       "value": "normal",
       "label": "Normal"
      },
      {
       "value": "fast",
       "label": "Fast"
      }
     ],
     "group": "Play"
    },
    {
     "key": "show_score",
     "title": "Score card after answers",
     "type": "toggle",
     "default": true,
     "group": "Play"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#00dcff",
     "group": "Colours"
    },
    {
     "key": "correct",
     "title": "Correct",
     "type": "color",
     "default": "#00ff78",
     "group": "Colours"
    },
    {
     "key": "wrong",
     "title": "Wrong",
     "type": "color",
     "default": "#ff143c",
     "group": "Colours"
    }
   ],
   "game": {
    "max_players": 1,
    "controls": [],
    "modes": [],
    "maps": [],
    "themes": []
   }
  },
  {
   "id": "tictactoe",
   "name": "X and 0",
   "category": "games",
   "icon": "hash",
   "description": "Tic-tac-toe for 1 or 2 players. Play X against the AI (arrows move, A places), or let a friend join from their phone on your Wi-Fi as O by scanning the panel's QR code. Variants: vanishing marks and misère.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/tictactoe.html",
   "preview": "media/apps/tictactoe.gif",
   "poster": "media/apps/tictactoe.png",
   "og_image": "media/apps/og/tictactoe.png",
   "source": "src/deskdot/apps/games_board.py",
   "actions": [
    {
     "id": "restart",
     "label": "Restart"
    },
    {
     "id": "demo",
     "label": "Autoplay"
    }
   ],
   "settings": [
    {
     "key": "speed",
     "title": "Speed",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 10
    },
    {
     "key": "theme",
     "title": "Theme",
     "type": "choice",
     "default": "chalk",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "neon",
       "label": "Neon"
      },
      {
       "value": "retro",
       "label": "Retro"
      },
      {
       "value": "mono",
       "label": "Mono"
      },
      {
       "value": "chalk",
       "label": "Chalkboard"
      },
      {
       "value": "arcade",
       "label": "Arcade"
      }
     ]
    },
    {
     "key": "skill",
     "title": "AI skill",
     "type": "number",
     "description": "How well the demo AI plays",
     "default": 0.8,
     "min": 0.0,
     "max": 1.0
    },
    {
     "key": "show_score",
     "title": "Show score",
     "type": "toggle",
     "default": true
    },
    {
     "key": "effects",
     "title": "Effects",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Full (particles, shake, flashes)"
      },
      {
       "value": "calm",
       "label": "Calm (no shake or flashes)"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      }
     ],
     "group": "Graphics"
    },
    {
     "key": "intro_outro",
     "title": "Intro & results screens",
     "type": "toggle",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "attract",
     "title": "Play itself when idle",
     "type": "toggle",
     "description": "The AI plays while nobody does (great in a playlist)",
     "default": true,
     "group": "Game flow"
    },
    {
     "key": "pilot",
     "title": "Plays itself with",
     "type": "choice",
     "default": "ai",
     "options": [
      {
       "value": "ai",
       "label": "Built-in AI"
      },
      {
       "value": "fly",
       "label": "Fruit-fly brain (sees only the pixels; docs/FLY_BRAIN.md)"
      }
     ],
     "group": "Game flow"
    },
    {
     "key": "first_to",
     "title": "Match: first to",
     "type": "integer",
     "description": "Round wins needed to take a match started from the menu",
     "default": 3,
     "min": 1,
     "max": 9,
     "group": "Game"
    },
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "solo",
     "options": [
      {
       "value": "solo",
       "label": "Vs AI"
      },
      {
       "value": "duel",
       "label": "Vs Friend"
      }
     ],
     "group": "Game"
    },
    {
     "key": "map",
     "title": "Map",
     "type": "choice",
     "default": "classic",
     "options": [
      {
       "value": "classic",
       "label": "Classic"
      },
      {
       "value": "vanish",
       "label": "Vanishing"
      },
      {
       "value": "misere",
       "label": "Misere"
      }
     ],
     "group": "Game"
    },
    {
     "key": "players",
     "title": "Players",
     "type": "integer",
     "default": 1,
     "min": 1,
     "max": 2,
     "group": "Game"
    }
   ],
   "game": {
    "max_players": 2,
    "controls": [
     "dpad",
     "gamepad"
    ],
    "modes": [
     {
      "id": "solo",
      "name": "Vs AI",
      "min_players": 1,
      "max_players": 1,
      "teams": "solo"
     },
     {
      "id": "duel",
      "name": "Vs Friend",
      "min_players": 2,
      "max_players": 2,
      "teams": "ffa"
     }
    ],
    "maps": [
     {
      "id": "classic",
      "label": "Classic"
     },
     {
      "id": "vanish",
      "label": "Vanishing"
     },
     {
      "id": "misere",
      "label": "Misere"
     }
    ],
    "themes": [
     {
      "value": "classic",
      "label": "Classic"
     },
     {
      "value": "neon",
      "label": "Neon"
     },
     {
      "value": "retro",
      "label": "Retro"
     },
     {
      "value": "mono",
      "label": "Mono"
     },
     {
      "value": "chalk",
      "label": "Chalkboard"
     },
     {
      "value": "arcade",
      "label": "Arcade"
     }
    ]
   }
  },
  {
   "id": "camera",
   "name": "Camera Mirror",
   "category": "media",
   "icon": "camera",
   "description": "Your webcam on the LEDs — follows your face, with pop-art, neon, thermal and 1-bit looks.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/camera.html",
   "preview": "media/apps/camera.gif",
   "poster": "media/apps/camera.png",
   "og_image": "media/apps/og/camera.png",
   "source": "src/deskdot/apps/live.py",
   "actions": [],
   "settings": [
    {
     "key": "auto_exposure",
     "title": "Auto exposure",
     "type": "toggle",
     "description": "Stretch levels to the content",
     "default": true,
     "group": "Exposure"
    },
    {
     "key": "exposure",
     "title": "Exposure (EV)",
     "type": "number",
     "description": "Manual, or on top of auto",
     "default": 0.0,
     "min": -3.0,
     "max": 3.0,
     "group": "Exposure"
    },
    {
     "key": "black_point",
     "title": "Black point %",
     "type": "integer",
     "default": 3,
     "min": 0,
     "max": 60,
     "group": "Exposure"
    },
    {
     "key": "white_point",
     "title": "White point %",
     "type": "integer",
     "default": 99,
     "min": 80,
     "max": 100,
     "group": "Exposure"
    },
    {
     "key": "brightness",
     "title": "Brightness",
     "type": "number",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Exposure"
    },
    {
     "key": "contrast",
     "title": "Contrast",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 3.0,
     "group": "Exposure"
    },
    {
     "key": "gamma",
     "title": "Gamma",
     "type": "number",
     "description": ">1 darker mid-tones",
     "default": 1.0,
     "min": 0.3,
     "max": 3.0,
     "group": "Exposure"
    },
    {
     "key": "saturation",
     "title": "Saturation",
     "type": "number",
     "default": 1.35,
     "min": 0.0,
     "max": 3.0,
     "group": "Colour"
    },
    {
     "key": "hue",
     "title": "Hue shift (°)",
     "type": "integer",
     "default": 0,
     "min": -180,
     "max": 180,
     "group": "Colour"
    },
    {
     "key": "temperature",
     "title": "White balance",
     "type": "number",
     "description": "Cool ← → warm",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Colour"
    },
    {
     "key": "tint",
     "title": "Tint",
     "type": "number",
     "description": "Green ← → magenta",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Colour"
    },
    {
     "key": "sharpness",
     "title": "Sharpness",
     "type": "number",
     "default": 0.5,
     "min": 0.0,
     "max": 3.0,
     "group": "Detail & effects"
    },
    {
     "key": "posterize",
     "title": "Posterise levels",
     "type": "integer",
     "description": "0 = off",
     "default": 0,
     "min": 0,
     "max": 8,
     "group": "Detail & effects"
    },
    {
     "key": "invert",
     "title": "Invert",
     "type": "toggle",
     "default": false,
     "group": "Detail & effects"
    },
    {
     "key": "flip_h",
     "title": "Mirror (selfie view)",
     "type": "toggle",
     "default": true,
     "group": "Framing"
    },
    {
     "key": "flip_v",
     "title": "Flip vertical",
     "type": "toggle",
     "default": false,
     "group": "Framing"
    },
    {
     "key": "rotate",
     "title": "Rotate",
     "type": "choice",
     "default": "0",
     "options": [
      {
       "value": "0",
       "label": "0°"
      },
      {
       "value": "90",
       "label": "90°"
      },
      {
       "value": "180",
       "label": "180°"
      },
      {
       "value": "270",
       "label": "270°"
      }
     ],
     "group": "Framing"
    },
    {
     "key": "zoom",
     "title": "Zoom",
     "type": "number",
     "default": 1.0,
     "min": 1.0,
     "max": 8.0,
     "group": "Framing"
    },
    {
     "key": "pan_x",
     "title": "Pan ←→",
     "type": "number",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Framing"
    },
    {
     "key": "pan_y",
     "title": "Pan ↑↓",
     "type": "number",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Framing"
    },
    {
     "key": "style",
     "title": "Look",
     "type": "choice",
     "default": "natural",
     "options": [
      {
       "value": "natural",
       "label": "Natural"
      },
      {
       "value": "pop",
       "label": "Pop art"
      },
      {
       "value": "neon",
       "label": "Neon edges"
      },
      {
       "value": "thermal",
       "label": "Thermal"
      },
      {
       "value": "mono",
       "label": "1-bit ember"
      }
     ]
    },
    {
     "key": "track_face",
     "title": "Follow my face",
     "type": "toggle",
     "default": true
    },
    {
     "key": "camera",
     "title": "Camera index",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 5,
     "group": "Camera hardware"
    },
    {
     "key": "hw_auto_exposure",
     "title": "Camera auto exposure",
     "type": "toggle",
     "default": true,
     "group": "Camera hardware"
    },
    {
     "key": "hw_exposure",
     "title": "Camera exposure",
     "type": "integer",
     "description": "When auto is off; lower = darker",
     "default": -6,
     "min": -13,
     "max": 0,
     "group": "Camera hardware"
    },
    {
     "key": "hw_gain",
     "title": "Camera gain (ISO)",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 255,
     "group": "Camera hardware"
    },
    {
     "key": "hw_auto_wb",
     "title": "Camera auto white balance",
     "type": "toggle",
     "default": true,
     "group": "Camera hardware"
    },
    {
     "key": "wb_kelvin",
     "title": "White balance (K)",
     "type": "integer",
     "default": 4600,
     "min": 2800,
     "max": 6500,
     "group": "Camera hardware"
    },
    {
     "key": "autofocus",
     "title": "Autofocus",
     "type": "toggle",
     "default": true,
     "group": "Camera hardware"
    },
    {
     "key": "focus",
     "title": "Manual focus",
     "type": "integer",
     "default": 0,
     "min": 0,
     "max": 255,
     "group": "Camera hardware"
    }
   ]
  },
  {
   "id": "gallery",
   "name": "Gallery",
   "category": "media",
   "icon": "images",
   "description": "Your uploads — photos are LED-calibrated, pixel art stays crisp.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/gallery.html",
   "preview": "media/apps/gallery.gif",
   "poster": "media/apps/gallery.png",
   "og_image": "media/apps/og/gallery.png",
   "source": "src/deskdot/apps/gallery.py",
   "actions": [],
   "settings": [
    {
     "key": "media",
     "title": "Media",
     "type": "text",
     "default": ""
    },
    {
     "key": "fit",
     "title": "Fit",
     "type": "choice",
     "default": "contain",
     "options": [
      {
       "value": "contain",
       "label": "Fit"
      },
      {
       "value": "cover",
       "label": "Fill (crop)"
      },
      {
       "value": "stretch",
       "label": "Stretch"
      }
     ]
    },
    {
     "key": "profile",
     "title": "LED colour",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto"
      },
      {
       "value": "vibrant",
       "label": "Vibrant"
      },
      {
       "value": "natural",
       "label": "Natural"
      },
      {
       "value": "raw",
       "label": "Raw 1:1"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Animation speed",
     "type": "number",
     "default": 1.0,
     "min": 0.25,
     "max": 4.0
    }
   ]
  },
  {
   "id": "mediaserver",
   "name": "Media Server",
   "category": "media",
   "icon": "clapperboard",
   "description": "Latest Plex or Jellyfin additions as posters, plus who's streaming now.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/mediaserver.html",
   "preview": "media/apps/mediaserver.gif",
   "poster": "media/apps/mediaserver.png",
   "og_image": "media/apps/og/mediaserver.png",
   "source": "src/deskdot/apps/mediaserver.py",
   "actions": [],
   "settings": [
    {
     "key": "server",
     "title": "Server",
     "type": "choice",
     "default": "plex",
     "options": [
      {
       "value": "plex",
       "label": "Plex"
      },
      {
       "value": "jellyfin",
       "label": "Jellyfin"
      }
     ],
     "group": "Connection"
    },
    {
     "key": "host",
     "title": "Host",
     "type": "text",
     "description": "e.g. 192.168.1.20 (blank = not set up)",
     "default": "",
     "max_length": 120,
     "group": "Connection"
    },
    {
     "key": "port",
     "title": "Port",
     "type": "integer",
     "description": "0 = default (Plex 32400, Jellyfin 8096)",
     "default": 0,
     "min": 0,
     "max": 65535,
     "group": "Connection"
    },
    {
     "key": "token",
     "title": "Token / API key",
     "type": "text",
     "description": "Plex: X-Plex-Token. Jellyfin: Dashboard → API Keys. Sent as a header, never logged.",
     "default": "",
     "max_length": 200,
     "group": "Connection"
    },
    {
     "key": "user",
     "title": "Jellyfin user",
     "type": "text",
     "description": "Blank = the first user",
     "default": "",
     "max_length": 60,
     "group": "Connection"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "poster",
     "options": [
      {
       "value": "poster",
       "label": "Poster"
      },
      {
       "value": "shelf",
       "label": "Shelf"
      },
      {
       "value": "streams",
       "label": "Now streaming"
      }
     ],
     "group": "Display"
    },
    {
     "key": "rotate",
     "title": "Seconds per item",
     "type": "integer",
     "default": 8,
     "min": 3,
     "max": 60,
     "group": "Display"
    },
    {
     "key": "sessions",
     "title": "Show now-streaming count",
     "type": "toggle",
     "description": "Polls sessions every minute",
     "default": true,
     "group": "Display"
    }
   ]
  },
  {
   "id": "nowplaying",
   "name": "Now Playing",
   "category": "media",
   "icon": "music",
   "description": "Spotify / YouTube Music / any player: album art, karaoke lyrics, vinyl, spectrum and more.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/nowplaying.html",
   "preview": "media/apps/nowplaying.gif",
   "poster": "media/apps/nowplaying.png",
   "og_image": "media/apps/og/nowplaying.png",
   "source": "src/deskdot/apps/nowplaying.py",
   "actions": [
    {
     "id": "prev",
     "label": "Previous"
    },
    {
     "id": "toggle",
     "label": "Play/Pause"
    },
    {
     "id": "next",
     "label": "Next"
    }
   ],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "karaoke",
     "options": [
      {
       "value": "karaoke",
       "label": "Karaoke"
      },
      {
       "value": "lyrics",
       "label": "Synced lyrics"
      },
      {
       "value": "lyrics_art",
       "label": "Lyrics over art"
      },
      {
       "value": "cover",
       "label": "Album art"
      },
      {
       "value": "cover_text",
       "label": "Art + title"
      },
      {
       "value": "live_art",
       "label": "Living art"
      },
      {
       "value": "vinyl",
       "label": "Vinyl"
      },
      {
       "value": "split",
       "label": "Art + info"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      },
      {
       "value": "palette",
       "label": "Colour wash"
      },
      {
       "value": "spectrum_art",
       "label": "Spectrum"
      }
     ]
    },
    {
     "key": "take_over",
     "title": "Take over while music plays",
     "type": "toggle",
     "description": "In the playlist",
     "default": true
    },
    {
     "key": "offset",
     "title": "Lyric offset (s)",
     "type": "number",
     "description": "Positive = earlier",
     "default": 0.3,
     "min": -5,
     "max": 5,
     "group": "Lyrics"
    },
    {
     "key": "color",
     "title": "Lyric colour",
     "type": "color",
     "description": "Current line in the Synced lyrics layout",
     "default": "#ffd600"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "description": "Progress and equaliser (unless themed from art)",
     "default": "#00ff8c"
    },
    {
     "key": "fallback_cover",
     "title": "Show art when no lyrics",
     "type": "toggle",
     "default": true
    },
    {
     "key": "karaoke_color",
     "title": "Sung colour",
     "type": "color",
     "default": "#ffd600",
     "group": "Lyrics"
    },
    {
     "key": "karaoke_dim",
     "title": "Unsung colour",
     "type": "color",
     "default": "#5a5a78",
     "group": "Lyrics"
    },
    {
     "key": "font",
     "title": "Lyric size",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto (big when it fits)"
      },
      {
       "value": "small",
       "label": "Big 7 px"
      },
      {
       "value": "tiny",
       "label": "Compact 5 px"
      }
     ],
     "group": "Lyrics"
    },
    {
     "key": "lyric_align",
     "title": "Lyric alignment",
     "type": "choice",
     "default": "center",
     "options": [
      {
       "value": "center",
       "label": "Centre"
      },
      {
       "value": "left",
       "label": "Left"
      }
     ],
     "group": "Lyrics"
    },
    {
     "key": "show_progress",
     "title": "Progress bar",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "show_time",
     "title": "Show time",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "time_mode",
     "title": "Time",
     "type": "choice",
     "default": "elapsed",
     "options": [
      {
       "value": "elapsed",
       "label": "Elapsed"
      },
      {
       "value": "remaining",
       "label": "Remaining"
      }
     ],
     "group": "Display"
    },
    {
     "key": "scroll_speed",
     "title": "Scroll speed (px/s)",
     "type": "number",
     "default": 12.0,
     "min": 6,
     "max": 24,
     "group": "Display"
    },
    {
     "key": "idle_screen",
     "title": "When nothing plays",
     "type": "choice",
     "default": "no_music",
     "options": [
      {
       "value": "no_music",
       "label": "NO MUSIC"
      },
      {
       "value": "clock",
       "label": "Clock"
      },
      {
       "value": "last_art",
       "label": "Last album art"
      },
      {
       "value": "nothing",
       "label": "Nothing"
      }
     ],
     "group": "Display"
    },
    {
     "key": "art_brightness",
     "title": "Art brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0,
     "group": "Art"
    },
    {
     "key": "art_dim",
     "title": "Art behind lyrics",
     "type": "number",
     "description": "Background level for Lyrics over art",
     "default": 0.3,
     "min": 0.05,
     "max": 0.7,
     "group": "Art"
    },
    {
     "key": "theme_from_art",
     "title": "Colours from album art",
     "type": "toggle",
     "description": "Sung colour and accent follow the cover's palette",
     "default": true,
     "group": "Art"
    },
    {
     "key": "beat_sync",
     "title": "Pulse with the beat",
     "type": "toggle",
     "description": "Living art breathes with system audio (starts audio capture)",
     "default": false,
     "group": "Art"
    }
   ]
  },
  {
   "id": "photoframe",
   "name": "Photo Frame",
   "category": "media",
   "icon": "frame",
   "description": "A slideshow of public-domain art, nature photos, dogs, cats, foxes and ducks.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/photoframe.html",
   "preview": "media/apps/photoframe.gif",
   "poster": "media/apps/photoframe.png",
   "og_image": "media/apps/og/photoframe.png",
   "source": "src/deskdot/apps/photoframe.py",
   "actions": [
    {
     "id": "next",
     "label": "Next photo"
    }
   ],
   "settings": [
    {
     "key": "art_met",
     "title": "Art: The Met",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "art_cleveland",
     "title": "Art: Cleveland Museum",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "picsum",
     "title": "Nature & random photos",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "dogs",
     "title": "Dogs",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "cats",
     "title": "Cats",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "foxes",
     "title": "Foxes",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "ducks",
     "title": "Ducks",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "art_query",
     "title": "Met search",
     "type": "text",
     "description": "What to look for in The Met's highlights, e.g. painting, landscape, flowers, cat",
     "default": "painting",
     "max_length": 40,
     "group": "Sources"
    },
    {
     "key": "dog_breed",
     "title": "Dog breed",
     "type": "text",
     "description": "Empty = any. e.g. husky, corgi, golden retriever, shiba",
     "default": "",
     "max_length": 40,
     "group": "Sources"
    },
    {
     "key": "interval",
     "title": "Seconds per photo",
     "type": "integer",
     "default": 20,
     "min": 5,
     "max": 600,
     "group": "Show"
    },
    {
     "key": "motion",
     "title": "Motion",
     "type": "choice",
     "default": "kenburns",
     "options": [
      {
       "value": "still",
       "label": "Still"
      },
      {
       "value": "kenburns",
       "label": "Ken Burns"
      }
     ],
     "group": "Show"
    },
    {
     "key": "crossfade",
     "title": "Crossfade",
     "type": "toggle",
     "default": true,
     "group": "Show"
    },
    {
     "key": "caption",
     "title": "Caption (title · artist)",
     "type": "choice",
     "default": "intro",
     "options": [
      {
       "value": "off",
       "label": "Off"
      },
      {
       "value": "intro",
       "label": "Scroll once"
      },
      {
       "value": "always",
       "label": "Always"
      }
     ],
     "group": "Show"
    },
    {
     "key": "framing",
     "title": "Framing",
     "type": "choice",
     "default": "smart",
     "options": [
      {
       "value": "smart",
       "label": "Smart crop"
      },
      {
       "value": "center",
       "label": "Centre crop"
      },
      {
       "value": "fit",
       "label": "Fit (letterbox)"
      }
     ],
     "group": "Image"
    },
    {
     "key": "profile",
     "title": "LED colour",
     "type": "choice",
     "default": "vibrant",
     "options": [
      {
       "value": "vibrant",
       "label": "Vibrant"
      },
      {
       "value": "natural",
       "label": "Natural"
      }
     ],
     "group": "Image"
    }
   ]
  },
  {
   "id": "mirror",
   "name": "Screen Mirror",
   "category": "media",
   "icon": "monitor-up",
   "description": "Your screen, a window, the area under the cursor, or ambient colours — live on the panel.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/mirror.html",
   "preview": "media/apps/mirror.gif",
   "poster": "media/apps/mirror.png",
   "og_image": "media/apps/og/mirror.png",
   "source": "src/deskdot/apps/live.py",
   "actions": [],
   "settings": [
    {
     "key": "auto_exposure",
     "title": "Auto exposure",
     "type": "toggle",
     "description": "Stretch levels to the content",
     "default": true,
     "group": "Exposure"
    },
    {
     "key": "exposure",
     "title": "Exposure (EV)",
     "type": "number",
     "description": "Manual, or on top of auto",
     "default": 0.0,
     "min": -3.0,
     "max": 3.0,
     "group": "Exposure"
    },
    {
     "key": "black_point",
     "title": "Black point %",
     "type": "integer",
     "description": "High = dark UIs go black",
     "default": 35,
     "min": 0,
     "max": 60,
     "group": "Exposure"
    },
    {
     "key": "white_point",
     "title": "White point %",
     "type": "integer",
     "default": 99,
     "min": 80,
     "max": 100,
     "group": "Exposure"
    },
    {
     "key": "brightness",
     "title": "Brightness",
     "type": "number",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Exposure"
    },
    {
     "key": "contrast",
     "title": "Contrast",
     "type": "number",
     "default": 1.15,
     "min": 0.2,
     "max": 3.0,
     "group": "Exposure"
    },
    {
     "key": "gamma",
     "title": "Gamma",
     "type": "number",
     "description": ">1 darker mid-tones",
     "default": 1.0,
     "min": 0.3,
     "max": 3.0,
     "group": "Exposure"
    },
    {
     "key": "saturation",
     "title": "Saturation",
     "type": "number",
     "default": 1.35,
     "min": 0.0,
     "max": 3.0,
     "group": "Colour"
    },
    {
     "key": "hue",
     "title": "Hue shift (°)",
     "type": "integer",
     "default": 0,
     "min": -180,
     "max": 180,
     "group": "Colour"
    },
    {
     "key": "temperature",
     "title": "White balance",
     "type": "number",
     "description": "Cool ← → warm",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Colour"
    },
    {
     "key": "tint",
     "title": "Tint",
     "type": "number",
     "description": "Green ← → magenta",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Colour"
    },
    {
     "key": "sharpness",
     "title": "Sharpness",
     "type": "number",
     "default": 0.5,
     "min": 0.0,
     "max": 3.0,
     "group": "Detail & effects"
    },
    {
     "key": "posterize",
     "title": "Posterise levels",
     "type": "integer",
     "description": "0 = off",
     "default": 0,
     "min": 0,
     "max": 8,
     "group": "Detail & effects"
    },
    {
     "key": "invert",
     "title": "Invert",
     "type": "toggle",
     "default": false,
     "group": "Detail & effects"
    },
    {
     "key": "flip_h",
     "title": "Flip horizontal",
     "type": "toggle",
     "default": false,
     "group": "Framing"
    },
    {
     "key": "flip_v",
     "title": "Flip vertical",
     "type": "toggle",
     "default": false,
     "group": "Framing"
    },
    {
     "key": "rotate",
     "title": "Rotate",
     "type": "choice",
     "default": "0",
     "options": [
      {
       "value": "0",
       "label": "0°"
      },
      {
       "value": "90",
       "label": "90°"
      },
      {
       "value": "180",
       "label": "180°"
      },
      {
       "value": "270",
       "label": "270°"
      }
     ],
     "group": "Framing"
    },
    {
     "key": "zoom",
     "title": "Zoom",
     "type": "number",
     "default": 1.0,
     "min": 1.0,
     "max": 8.0,
     "group": "Framing"
    },
    {
     "key": "pan_x",
     "title": "Pan ←→",
     "type": "number",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Framing"
    },
    {
     "key": "pan_y",
     "title": "Pan ↑↓",
     "type": "number",
     "default": 0.0,
     "min": -1.0,
     "max": 1.0,
     "group": "Framing"
    },
    {
     "key": "mode",
     "title": "Source",
     "type": "choice",
     "default": "full",
     "options": [
      {
       "value": "full",
       "label": "Whole screen"
      },
      {
       "value": "window",
       "label": "Active window"
      },
      {
       "value": "cursor",
       "label": "Around cursor"
      },
      {
       "value": "ambilight",
       "label": "Ambilight colours"
      }
     ]
    },
    {
     "key": "fit",
     "title": "Fit",
     "type": "choice",
     "default": "contain",
     "options": [
      {
       "value": "contain",
       "label": "Fit whole screen"
      },
      {
       "value": "cover",
       "label": "Fill (crop)"
      }
     ]
    },
    {
     "key": "cursor_box",
     "title": "Cursor box (px)",
     "type": "integer",
     "description": "Smaller = more magnified",
     "default": 96,
     "min": 48,
     "max": 800
    }
   ]
  },
  {
   "id": "visualizer",
   "name": "Visualizer",
   "category": "media",
   "icon": "audio-lines",
   "description": "Reacts to whatever your PC is playing — Spotify, YouTube, games. No mic needed.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/visualizer.html",
   "preview": "media/apps/visualizer.gif",
   "poster": "media/apps/visualizer.png",
   "og_image": "media/apps/og/visualizer.png",
   "source": "src/deskdot/apps/live.py",
   "actions": [],
   "settings": [
    {
     "key": "style",
     "title": "Style",
     "type": "choice",
     "default": "bars",
     "options": [
      {
       "value": "bars",
       "label": "Spectrum bars"
      },
      {
       "value": "mirror",
       "label": "Mirrored"
      },
      {
       "value": "radial",
       "label": "Radial burst"
      },
      {
       "value": "wave",
       "label": "Waveform"
      },
      {
       "value": "fire",
       "label": "Spectrum fire"
      },
      {
       "value": "rings",
       "label": "Pulse rings"
      }
     ]
    },
    {
     "key": "palette",
     "title": "Palette",
     "type": "choice",
     "default": "rainbow",
     "options": [
      {
       "value": "rainbow",
       "label": "Rainbow"
      },
      {
       "value": "heat",
       "label": "Heat"
      },
      {
       "value": "mono",
       "label": "Single colour"
      }
     ]
    },
    {
     "key": "color",
     "title": "Colour (single-colour palette)",
     "type": "color",
     "default": "#00ff8c"
    },
    {
     "key": "peaks",
     "title": "Peak dots",
     "type": "toggle",
     "default": true
    },
    {
     "key": "gain",
     "title": "Sensitivity",
     "type": "number",
     "default": 1.0,
     "min": 0.3,
     "max": 3.0
    }
   ]
  },
  {
   "id": "pet",
   "name": "Pet",
   "category": "pets",
   "icon": "paw-print",
   "description": "A pixel pet or character that lives on the panel: wanders, naps, snacks and dances to music.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/pet.html",
   "preview": "media/apps/pet.gif",
   "poster": "media/apps/pet.png",
   "og_image": "media/apps/og/pet.png",
   "source": "src/deskdot/apps/pets.py",
   "actions": [],
   "settings": [
    {
     "key": "character",
     "title": "Character",
     "type": "choice",
     "default": "clawd",
     "options": [
      {
       "value": "clawd",
       "label": "Clawd"
      },
      {
       "value": "claude_mini",
       "label": "Clawd Mini"
      },
      {
       "value": "shiba",
       "label": "Shiba"
      },
      {
       "value": "pug",
       "label": "Pug"
      },
      {
       "value": "husky",
       "label": "Husky"
      },
      {
       "value": "dachshund",
       "label": "Dachshund"
      },
      {
       "value": "golden",
       "label": "Golden Retriever"
      },
      {
       "value": "corgi",
       "label": "Corgi"
      },
      {
       "value": "tabby",
       "label": "Orange Tabby"
      },
      {
       "value": "black_cat",
       "label": "Black Cat"
      },
      {
       "value": "calico",
       "label": "Calico"
      },
      {
       "value": "siamese",
       "label": "Siamese"
      },
      {
       "value": "kitten",
       "label": "Grey Kitten"
      },
      {
       "value": "bunny",
       "label": "Bunny"
      },
      {
       "value": "hamster",
       "label": "Hamster"
      },
      {
       "value": "panda",
       "label": "Panda"
      },
      {
       "value": "penguin",
       "label": "Penguin"
      },
      {
       "value": "frog",
       "label": "Frog"
      },
      {
       "value": "duck",
       "label": "Duckling"
      },
      {
       "value": "axolotl",
       "label": "Axolotl"
      },
      {
       "value": "fox",
       "label": "Fox"
      },
      {
       "value": "capybara",
       "label": "Capybara"
      },
      {
       "value": "volt_mouse",
       "label": "Volt Mouse"
      },
      {
       "value": "ember_lizard",
       "label": "Ember Lizard"
      },
      {
       "value": "bubble_turtle",
       "label": "Bubble Turtle"
      },
      {
       "value": "sprout_toad",
       "label": "Sprout Toad"
      },
      {
       "value": "grin_ghost",
       "label": "Grin Ghost"
      },
      {
       "value": "puff",
       "label": "Puff"
      },
      {
       "value": "drake",
       "label": "Drake"
      },
      {
       "value": "knight",
       "label": "Knight"
      },
      {
       "value": "archer",
       "label": "Archer"
      },
      {
       "value": "barbarian",
       "label": "Barbarian"
      },
      {
       "value": "giant",
       "label": "Giant"
      },
      {
       "value": "goblin",
       "label": "Goblin"
      },
      {
       "value": "wizard",
       "label": "Wizard"
      },
      {
       "value": "skeleton",
       "label": "Skeleton"
      },
      {
       "value": "mech_knight",
       "label": "Mech Knight"
      },
      {
       "value": "king",
       "label": "King"
      },
      {
       "value": "princess",
       "label": "Princess"
      },
      {
       "value": "br_soldier",
       "label": "BR Soldier"
      },
      {
       "value": "ct_operator",
       "label": "CT Operator"
      },
      {
       "value": "terrorist",
       "label": "Terrorist"
      },
      {
       "value": "agent",
       "label": "Agent Duelist"
      },
      {
       "value": "sniper",
       "label": "Sniper"
      },
      {
       "value": "mad_scientist",
       "label": "Mad Scientist"
      },
      {
       "value": "nervous_teen",
       "label": "Nervous Teen"
      },
      {
       "value": "crayon_kid",
       "label": "Crayon Kid"
      },
      {
       "value": "chem_teacher",
       "label": "Chem Teacher"
      },
      {
       "value": "hazmat",
       "label": "Hazmat Partner"
      },
      {
       "value": "vigilante",
       "label": "Masked Vigilante"
      },
      {
       "value": "chauffeur",
       "label": "Kung-Fu Chauffeur"
      },
      {
       "value": "footballer",
       "label": "Footballer"
      },
      {
       "value": "goalkeeper",
       "label": "Goalkeeper"
      }
     ]
    },
    {
     "key": "animation",
     "title": "Animation",
     "type": "choice",
     "default": "alive",
     "options": [
      {
       "value": "alive",
       "label": "Alive (auto behaviour)"
      },
      {
       "value": "idle",
       "label": "Idle"
      },
      {
       "value": "walk",
       "label": "Walk"
      },
      {
       "value": "run",
       "label": "Run"
      },
      {
       "value": "jump",
       "label": "Jump"
      },
      {
       "value": "dance",
       "label": "Dance"
      },
      {
       "value": "sleep",
       "label": "Sleep"
      },
      {
       "value": "sit",
       "label": "Sit"
      },
      {
       "value": "eat",
       "label": "Eat"
      },
      {
       "value": "happy",
       "label": "Happy"
      },
      {
       "value": "sad",
       "label": "Sad"
      },
      {
       "value": "wave",
       "label": "Wave"
      },
      {
       "value": "kick",
       "label": "Kick"
      },
      {
       "value": "work",
       "label": "Work"
      },
      {
       "value": "think",
       "label": "Think"
      },
      {
       "value": "shoot",
       "label": "Shoot"
      },
      {
       "value": "reload",
       "label": "Reload"
      },
      {
       "value": "crouch",
       "label": "Crouch"
      },
      {
       "value": "attack",
       "label": "Attack"
      },
      {
       "value": "cast",
       "label": "Cast"
      }
     ]
    },
    {
     "key": "scale",
     "title": "Size",
     "type": "choice",
     "default": "2",
     "options": [
      {
       "value": "1",
       "label": "Small"
      },
      {
       "value": "2",
       "label": "Large (2x)"
      }
     ]
    },
    {
     "key": "scene",
     "title": "Scene",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "grass",
       "label": "Meadow"
      },
      {
       "value": "room",
       "label": "Cosy room"
      },
      {
       "value": "night",
       "label": "Night sky"
      },
      {
       "value": "beach",
       "label": "Beach"
      },
      {
       "value": "space",
       "label": "Space"
      },
      {
       "value": "stage",
       "label": "Stage"
      },
      {
       "value": "snow",
       "label": "Snow"
      }
     ]
    },
    {
     "key": "accessory",
     "title": "Accessory",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "cap",
       "label": "Cap"
      },
      {
       "value": "beanie",
       "label": "Beanie"
      },
      {
       "value": "crown",
       "label": "Crown"
      },
      {
       "value": "party_hat",
       "label": "Party hat"
      },
      {
       "value": "headphones",
       "label": "Headphones"
      },
      {
       "value": "sunglasses",
       "label": "Sunglasses"
      },
      {
       "value": "halo",
       "label": "Halo"
      },
      {
       "value": "bow",
       "label": "Bow"
      },
      {
       "value": "scarf",
       "label": "Scarf"
      },
      {
       "value": "helmet",
       "label": "Helmet"
      },
      {
       "value": "wizard_hat",
       "label": "Wizard hat"
      },
      {
       "value": "santa_hat",
       "label": "Santa hat"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.25,
     "max": 3.0
    },
    {
     "key": "flip",
     "title": "Face left",
     "type": "toggle",
     "default": false
    },
    {
     "key": "music_sync",
     "title": "Dance to music",
     "type": "toggle",
     "description": "Dance on the beat when music is playing.",
     "default": false
    },
    {
     "key": "name",
     "title": "Name tag",
     "type": "text",
     "default": "",
     "max_length": 10
    },
    {
     "key": "use_custom_colors",
     "title": "Custom colours",
     "type": "toggle",
     "default": false,
     "group": "Colours"
    },
    {
     "key": "primary",
     "title": "Primary",
     "type": "color",
     "default": "#ff6024",
     "group": "Colours"
    },
    {
     "key": "secondary",
     "title": "Secondary",
     "type": "color",
     "default": "#ffe0b4",
     "group": "Colours"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#1e8cff",
     "group": "Colours"
    }
   ]
  },
  {
   "id": "petworld",
   "name": "Pet World",
   "category": "pets",
   "icon": "trees",
   "description": "Lofi pets living in a scrolling little world — working, snacking, football, naps, dancing.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/petworld.html",
   "preview": "media/apps/petworld.gif",
   "poster": "media/apps/petworld.png",
   "og_image": "media/apps/og/petworld.png",
   "source": "src/deskdot/apps/petworld.py",
   "actions": [],
   "settings": [
    {
     "key": "character",
     "title": "Pet",
     "type": "choice",
     "default": "clawd",
     "options": [
      {
       "value": "clawd",
       "label": "Claude · Clawd"
      },
      {
       "value": "claude_mini",
       "label": "Claude · Clawd Mini"
      },
      {
       "value": "shiba",
       "label": "Dogs · Shiba"
      },
      {
       "value": "pug",
       "label": "Dogs · Pug"
      },
      {
       "value": "husky",
       "label": "Dogs · Husky"
      },
      {
       "value": "dachshund",
       "label": "Dogs · Dachshund"
      },
      {
       "value": "golden",
       "label": "Dogs · Golden Retriever"
      },
      {
       "value": "corgi",
       "label": "Dogs · Corgi"
      },
      {
       "value": "tabby",
       "label": "Cats · Orange Tabby"
      },
      {
       "value": "black_cat",
       "label": "Cats · Black Cat"
      },
      {
       "value": "calico",
       "label": "Cats · Calico"
      },
      {
       "value": "siamese",
       "label": "Cats · Siamese"
      },
      {
       "value": "kitten",
       "label": "Cats · Grey Kitten"
      },
      {
       "value": "bunny",
       "label": "Critters · Bunny"
      },
      {
       "value": "hamster",
       "label": "Critters · Hamster"
      },
      {
       "value": "panda",
       "label": "Critters · Panda"
      },
      {
       "value": "penguin",
       "label": "Critters · Penguin"
      },
      {
       "value": "frog",
       "label": "Critters · Frog"
      },
      {
       "value": "duck",
       "label": "Critters · Duckling"
      },
      {
       "value": "axolotl",
       "label": "Critters · Axolotl"
      },
      {
       "value": "fox",
       "label": "Critters · Fox"
      },
      {
       "value": "capybara",
       "label": "Critters · Capybara"
      },
      {
       "value": "volt_mouse",
       "label": "Monsters · Volt Mouse"
      },
      {
       "value": "ember_lizard",
       "label": "Monsters · Ember Lizard"
      },
      {
       "value": "bubble_turtle",
       "label": "Monsters · Bubble Turtle"
      },
      {
       "value": "sprout_toad",
       "label": "Monsters · Sprout Toad"
      },
      {
       "value": "grin_ghost",
       "label": "Monsters · Grin Ghost"
      },
      {
       "value": "puff",
       "label": "Monsters · Puff"
      },
      {
       "value": "drake",
       "label": "Monsters · Drake"
      },
      {
       "value": "knight",
       "label": "Troops · Knight"
      },
      {
       "value": "archer",
       "label": "Troops · Archer"
      },
      {
       "value": "barbarian",
       "label": "Troops · Barbarian"
      },
      {
       "value": "giant",
       "label": "Troops · Giant"
      },
      {
       "value": "goblin",
       "label": "Troops · Goblin"
      },
      {
       "value": "wizard",
       "label": "Troops · Wizard"
      },
      {
       "value": "skeleton",
       "label": "Troops · Skeleton"
      },
      {
       "value": "mech_knight",
       "label": "Troops · Mech Knight"
      },
      {
       "value": "king",
       "label": "Troops · King"
      },
      {
       "value": "princess",
       "label": "Troops · Princess"
      },
      {
       "value": "br_soldier",
       "label": "Shooters · BR Soldier"
      },
      {
       "value": "ct_operator",
       "label": "Shooters · CT Operator"
      },
      {
       "value": "terrorist",
       "label": "Shooters · Terrorist"
      },
      {
       "value": "agent",
       "label": "Shooters · Agent Duelist"
      },
      {
       "value": "sniper",
       "label": "Shooters · Sniper"
      },
      {
       "value": "mad_scientist",
       "label": "TV & Cartoons · Mad Scientist"
      },
      {
       "value": "nervous_teen",
       "label": "TV & Cartoons · Nervous Teen"
      },
      {
       "value": "crayon_kid",
       "label": "TV & Cartoons · Crayon Kid"
      },
      {
       "value": "chem_teacher",
       "label": "TV & Cartoons · Chem Teacher"
      },
      {
       "value": "hazmat",
       "label": "TV & Cartoons · Hazmat Partner"
      },
      {
       "value": "vigilante",
       "label": "TV & Cartoons · Masked Vigilante"
      },
      {
       "value": "chauffeur",
       "label": "TV & Cartoons · Kung-Fu Chauffeur"
      },
      {
       "value": "footballer",
       "label": "Sports · Footballer"
      },
      {
       "value": "goalkeeper",
       "label": "Sports · Goalkeeper"
      }
     ]
    },
    {
     "key": "buddy",
     "title": "Buddy",
     "type": "choice",
     "description": "Follows your pet around",
     "default": "shiba",
     "options": [
      {
       "value": "none",
       "label": "Nobody"
      },
      {
       "value": "clawd",
       "label": "Claude · Clawd"
      },
      {
       "value": "claude_mini",
       "label": "Claude · Clawd Mini"
      },
      {
       "value": "shiba",
       "label": "Dogs · Shiba"
      },
      {
       "value": "pug",
       "label": "Dogs · Pug"
      },
      {
       "value": "husky",
       "label": "Dogs · Husky"
      },
      {
       "value": "dachshund",
       "label": "Dogs · Dachshund"
      },
      {
       "value": "golden",
       "label": "Dogs · Golden Retriever"
      },
      {
       "value": "corgi",
       "label": "Dogs · Corgi"
      },
      {
       "value": "tabby",
       "label": "Cats · Orange Tabby"
      },
      {
       "value": "black_cat",
       "label": "Cats · Black Cat"
      },
      {
       "value": "calico",
       "label": "Cats · Calico"
      },
      {
       "value": "siamese",
       "label": "Cats · Siamese"
      },
      {
       "value": "kitten",
       "label": "Cats · Grey Kitten"
      },
      {
       "value": "bunny",
       "label": "Critters · Bunny"
      },
      {
       "value": "hamster",
       "label": "Critters · Hamster"
      },
      {
       "value": "panda",
       "label": "Critters · Panda"
      },
      {
       "value": "penguin",
       "label": "Critters · Penguin"
      },
      {
       "value": "frog",
       "label": "Critters · Frog"
      },
      {
       "value": "duck",
       "label": "Critters · Duckling"
      },
      {
       "value": "axolotl",
       "label": "Critters · Axolotl"
      },
      {
       "value": "fox",
       "label": "Critters · Fox"
      },
      {
       "value": "capybara",
       "label": "Critters · Capybara"
      },
      {
       "value": "volt_mouse",
       "label": "Monsters · Volt Mouse"
      },
      {
       "value": "ember_lizard",
       "label": "Monsters · Ember Lizard"
      },
      {
       "value": "bubble_turtle",
       "label": "Monsters · Bubble Turtle"
      },
      {
       "value": "sprout_toad",
       "label": "Monsters · Sprout Toad"
      },
      {
       "value": "grin_ghost",
       "label": "Monsters · Grin Ghost"
      },
      {
       "value": "puff",
       "label": "Monsters · Puff"
      },
      {
       "value": "drake",
       "label": "Monsters · Drake"
      },
      {
       "value": "knight",
       "label": "Troops · Knight"
      },
      {
       "value": "archer",
       "label": "Troops · Archer"
      },
      {
       "value": "barbarian",
       "label": "Troops · Barbarian"
      },
      {
       "value": "giant",
       "label": "Troops · Giant"
      },
      {
       "value": "goblin",
       "label": "Troops · Goblin"
      },
      {
       "value": "wizard",
       "label": "Troops · Wizard"
      },
      {
       "value": "skeleton",
       "label": "Troops · Skeleton"
      },
      {
       "value": "mech_knight",
       "label": "Troops · Mech Knight"
      },
      {
       "value": "king",
       "label": "Troops · King"
      },
      {
       "value": "princess",
       "label": "Troops · Princess"
      },
      {
       "value": "br_soldier",
       "label": "Shooters · BR Soldier"
      },
      {
       "value": "ct_operator",
       "label": "Shooters · CT Operator"
      },
      {
       "value": "terrorist",
       "label": "Shooters · Terrorist"
      },
      {
       "value": "agent",
       "label": "Shooters · Agent Duelist"
      },
      {
       "value": "sniper",
       "label": "Shooters · Sniper"
      },
      {
       "value": "mad_scientist",
       "label": "TV & Cartoons · Mad Scientist"
      },
      {
       "value": "nervous_teen",
       "label": "TV & Cartoons · Nervous Teen"
      },
      {
       "value": "crayon_kid",
       "label": "TV & Cartoons · Crayon Kid"
      },
      {
       "value": "chem_teacher",
       "label": "TV & Cartoons · Chem Teacher"
      },
      {
       "value": "hazmat",
       "label": "TV & Cartoons · Hazmat Partner"
      },
      {
       "value": "vigilante",
       "label": "TV & Cartoons · Masked Vigilante"
      },
      {
       "value": "chauffeur",
       "label": "TV & Cartoons · Kung-Fu Chauffeur"
      },
      {
       "value": "footballer",
       "label": "Sports · Footballer"
      },
      {
       "value": "goalkeeper",
       "label": "Sports · Goalkeeper"
      }
     ]
    },
    {
     "key": "world",
     "title": "World",
     "type": "choice",
     "default": "home",
     "options": [
      {
       "value": "home",
       "label": "Cosy room"
      },
      {
       "value": "park",
       "label": "Park & pitch"
      },
      {
       "value": "beach",
       "label": "Beach"
      },
      {
       "value": "space",
       "label": "Space station"
      },
      {
       "value": "city",
       "label": "City street"
      },
      {
       "value": "cafe",
       "label": "Lofi café"
      }
     ]
    },
    {
     "key": "accessory",
     "title": "Accessory",
     "type": "choice",
     "default": "none",
     "options": [
      {
       "value": "none",
       "label": "None"
      },
      {
       "value": "cap",
       "label": "Cap"
      },
      {
       "value": "beanie",
       "label": "Beanie"
      },
      {
       "value": "crown",
       "label": "Crown"
      },
      {
       "value": "party_hat",
       "label": "Party hat"
      },
      {
       "value": "headphones",
       "label": "Headphones"
      },
      {
       "value": "sunglasses",
       "label": "Sunglasses"
      },
      {
       "value": "halo",
       "label": "Halo"
      },
      {
       "value": "bow",
       "label": "Bow"
      },
      {
       "value": "scarf",
       "label": "Scarf"
      },
      {
       "value": "helmet",
       "label": "Helmet"
      },
      {
       "value": "wizard_hat",
       "label": "Wizard hat"
      },
      {
       "value": "santa_hat",
       "label": "Santa hat"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Pace",
     "type": "number",
     "default": 1.0,
     "min": 0.3,
     "max": 3.0
    },
    {
     "key": "time_of_day",
     "title": "Time of day",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Follow the clock"
      },
      {
       "value": "day",
       "label": "Always day"
      },
      {
       "value": "sunset",
       "label": "Always sunset"
      },
      {
       "value": "night",
       "label": "Always night"
      }
     ],
     "group": "Mood"
    },
    {
     "key": "sleep_at_night",
     "title": "Sleep at night",
     "type": "toggle",
     "default": true,
     "group": "Mood"
    },
    {
     "key": "weather",
     "title": "Weather",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Live weather"
      },
      {
       "value": "none",
       "label": "Clear"
      },
      {
       "value": "rain",
       "label": "Rain"
      },
      {
       "value": "snow",
       "label": "Snow"
      }
     ],
     "group": "Mood"
    },
    {
     "key": "lofi",
     "title": "Lofi warmth",
     "type": "toggle",
     "description": "Warm, soft colours",
     "default": true,
     "group": "Mood"
    },
    {
     "key": "music_sync",
     "title": "Dance to music",
     "type": "toggle",
     "default": true,
     "group": "Music"
    },
    {
     "key": "show_clock",
     "title": "Show the time",
     "type": "toggle",
     "default": false,
     "group": "Mood"
    },
    {
     "key": "wall",
     "title": "Wall colour",
     "type": "color",
     "description": "Cosy room",
     "default": "#1e4650",
     "group": "Mood"
    },
    {
     "key": "outline",
     "title": "Outline characters",
     "type": "toggle",
     "description": "Easier to see on busy scenery",
     "default": false
    },
    {
     "key": "camera",
     "title": "Camera",
     "type": "choice",
     "description": "Screens look smoother on the LEDs: the world only slides when the pet walks off-screen",
     "default": "rooms",
     "options": [
      {
       "value": "rooms",
       "label": "Screens (scenery stays still)"
      },
      {
       "value": "follow",
       "label": "Follow the pet (scrolls)"
      }
     ]
    }
   ]
  },
  {
   "id": "pokedex",
   "name": "Pokédex",
   "category": "pets",
   "icon": "sparkles",
   "description": "Pokémon of the day or a random one: crisp sprite, types, stats and Who's that Pokémon.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/pokedex.html",
   "preview": "media/apps/pokedex.gif",
   "poster": "media/apps/pokedex.png",
   "og_image": "media/apps/og/pokedex.png",
   "source": "src/deskdot/apps/pokedex.py",
   "actions": [
    {
     "id": "next",
     "label": "Another one"
    },
    {
     "id": "shiny",
     "label": "Toggle shiny"
    }
   ],
   "settings": [
    {
     "key": "mode",
     "title": "Which Pokémon",
     "type": "choice",
     "default": "daily",
     "options": [
      {
       "value": "daily",
       "label": "Pokémon of the day"
      },
      {
       "value": "random",
       "label": "Random"
      },
      {
       "value": "specific",
       "label": "Specific"
      }
     ],
     "group": "Pokémon"
    },
    {
     "key": "pokemon",
     "title": "Name or number",
     "type": "text",
     "description": "Used in Specific mode: pikachu, 25, mr-mime…",
     "default": "pikachu",
     "max_length": 30,
     "group": "Pokémon"
    },
    {
     "key": "generation",
     "title": "Generation",
     "type": "choice",
     "default": "all",
     "options": [
      {
       "value": "all",
       "label": "All generations"
      },
      {
       "value": "1",
       "label": "Gen 1"
      },
      {
       "value": "2",
       "label": "Gen 2"
      },
      {
       "value": "3",
       "label": "Gen 3"
      },
      {
       "value": "4",
       "label": "Gen 4"
      },
      {
       "value": "5",
       "label": "Gen 5"
      },
      {
       "value": "6",
       "label": "Gen 6"
      },
      {
       "value": "7",
       "label": "Gen 7"
      },
      {
       "value": "8",
       "label": "Gen 8"
      },
      {
       "value": "9",
       "label": "Gen 9"
      }
     ],
     "group": "Pokémon"
    },
    {
     "key": "every",
     "title": "New random every (s)",
     "type": "integer",
     "default": 120,
     "min": 10,
     "max": 3600,
     "group": "Pokémon"
    },
    {
     "key": "shiny",
     "title": "Colours",
     "type": "choice",
     "default": "off",
     "options": [
      {
       "value": "off",
       "label": "Normal"
      },
      {
       "value": "on",
       "label": "Shiny"
      },
      {
       "value": "rare",
       "label": "Rare shiny (1 in 16)"
      }
     ],
     "group": "Pokémon"
    },
    {
     "key": "view",
     "title": "Show",
     "type": "choice",
     "default": "cycle",
     "options": [
      {
       "value": "cycle",
       "label": "Card → info → stats"
      },
      {
       "value": "portrait",
       "label": "Portrait"
      },
      {
       "value": "info",
       "label": "Info"
      },
      {
       "value": "stats",
       "label": "Stats"
      },
      {
       "value": "who",
       "label": "Who's that Pokémon?"
      }
     ],
     "group": "Display"
    },
    {
     "key": "page_seconds",
     "title": "Seconds per page",
     "type": "integer",
     "default": 6,
     "min": 2,
     "max": 60,
     "group": "Display"
    },
    {
     "key": "guess_seconds",
     "title": "Who's that: seconds to guess",
     "type": "integer",
     "default": 6,
     "min": 2,
     "max": 60,
     "group": "Display"
    },
    {
     "key": "units",
     "title": "Units",
     "type": "choice",
     "default": "metric",
     "options": [
      {
       "value": "metric",
       "label": "Metric"
      },
      {
       "value": "imperial",
       "label": "Imperial"
      }
     ],
     "group": "Display"
    }
   ]
  },
  {
   "id": "activeapp",
   "name": "Active App",
   "category": "productivity",
   "icon": "app-window",
   "description": "The app you're using right now — its real icon, a matching animation and today's focus time.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/activeapp.html",
   "preview": "media/apps/activeapp.gif",
   "poster": "media/apps/activeapp.png",
   "og_image": "media/apps/og/activeapp.png",
   "source": "src/deskdot/apps/activeapp.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "icon",
     "options": [
      {
       "value": "icon",
       "label": "Icon"
      },
      {
       "value": "title",
       "label": "Title"
      },
      {
       "value": "focus",
       "label": "Focus"
      }
     ]
    },
    {
     "key": "take_over",
     "title": "Pop up when you switch apps",
     "type": "toggle",
     "description": "In the playlist",
     "default": false
    },
    {
     "key": "popup_seconds",
     "title": "Pop-up seconds",
     "type": "integer",
     "default": 5,
     "min": 2,
     "max": 30
    }
   ]
  },
  {
   "id": "anki",
   "name": "Anki",
   "category": "productivity",
   "icon": "layers",
   "description": "Anki cards due today (new, learning, review) with progress, and a word of the day.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/anki.html",
   "preview": "media/apps/anki.gif",
   "poster": "media/apps/anki.png",
   "og_image": "media/apps/og/anki.png",
   "source": "src/deskdot/apps/anki.py",
   "actions": [],
   "settings": [
    {
     "key": "host",
     "title": "Host",
     "type": "text",
     "default": "127.0.0.1",
     "max_length": 120,
     "group": "AnkiConnect"
    },
    {
     "key": "port",
     "title": "Port",
     "type": "integer",
     "description": "AnkiConnect defaults to 8765, which DeskDot already uses. Set \"webBindPort\": 8766 in Anki → Tools → Add-ons → AnkiConnect → Config and restart Anki.",
     "default": 8766,
     "min": 1,
     "max": 65535,
     "group": "AnkiConnect"
    },
    {
     "key": "key",
     "title": "API key (optional)",
     "type": "text",
     "description": "Only if you set \"apiKey\" in AnkiConnect's config. Never logged.",
     "default": "",
     "max_length": 120,
     "group": "AnkiConnect"
    },
    {
     "key": "deck",
     "title": "Deck",
     "type": "text",
     "description": "Exact deck name, e.g. Japanese::Core 2k. Blank = all decks",
     "default": "",
     "max_length": 120,
     "group": "Display"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "due",
     "options": [
      {
       "value": "due",
       "label": "Cards due"
      },
      {
       "value": "word",
       "label": "Word of the day"
      }
     ],
     "group": "Display"
    }
   ]
  },
  {
   "id": "calendar",
   "name": "Calendar",
   "category": "productivity",
   "icon": "calendar-clock",
   "description": "Next meeting from your ICS calendars: countdown ring, scrolling title and today's timeline.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/calendar.html",
   "preview": "media/apps/calendar.gif",
   "poster": "media/apps/calendar.png",
   "og_image": "media/apps/og/calendar.png",
   "source": "src/deskdot/apps/calendar.py",
   "actions": [],
   "settings": [
    {
     "key": "urls",
     "title": "ICS links",
     "type": "text",
     "description": "Secret iCal/ICS URLs (Google 'secret address in iCal format', Outlook 'published ICS'), one per line. They are never shown on the panel.",
     "default": "",
     "max_length": 2000
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "next",
     "options": [
      {
       "value": "next",
       "label": "Next meeting"
      },
      {
       "value": "agenda",
       "label": "Agenda"
      }
     ]
    },
    {
     "key": "lookahead",
     "title": "Look ahead (hours)",
     "type": "integer",
     "default": 12,
     "min": 1,
     "max": 72
    },
    {
     "key": "all_day",
     "title": "Include all-day events",
     "type": "toggle",
     "default": false
    },
    {
     "key": "take_over",
     "title": "Take over before a meeting",
     "type": "toggle",
     "default": true,
     "group": "Focus"
    },
    {
     "key": "take_over_minutes",
     "title": "Minutes before",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 30,
     "group": "Focus"
    },
    {
     "key": "hour24",
     "title": "24-hour times",
     "type": "toggle",
     "default": true
    }
   ]
  },
  {
   "id": "ci",
   "name": "CI Radiator",
   "category": "productivity",
   "icon": "git-branch",
   "description": "GitHub Actions status tiles for your repos, and a should-I-deploy verdict.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/ci.html",
   "preview": "media/apps/ci.gif",
   "poster": "media/apps/ci.png",
   "og_image": "media/apps/og/ci.png",
   "source": "src/deskdot/apps/ci.py",
   "actions": [],
   "settings": [
    {
     "key": "repos",
     "title": "Repositories",
     "type": "text",
     "description": "owner/repo, comma-separated (up to 16). GitHub URLs work too.",
     "default": "python/cpython",
     "max_length": 400,
     "group": "Repositories"
    },
    {
     "key": "token",
     "title": "GitHub token (optional)",
     "type": "text",
     "description": "Raises the limit from 60 to 5000 requests/h and allows private repos. Never logged.",
     "default": "",
     "max_length": 200,
     "group": "Repositories"
    },
    {
     "key": "branch",
     "title": "Branch",
     "type": "text",
     "description": "Blank = each repo's default branch",
     "default": "",
     "max_length": 100,
     "group": "Repositories"
    },
    {
     "key": "events",
     "title": "Runs to watch",
     "type": "choice",
     "default": "push",
     "options": [
      {
       "value": "push",
       "label": "Pushes"
      },
      {
       "value": "all",
       "label": "Any event"
      }
     ],
     "group": "Repositories"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "tiles",
     "options": [
      {
       "value": "tiles",
       "label": "Tiles"
      },
      {
       "value": "big",
       "label": "Big status"
      },
      {
       "value": "verdict",
       "label": "Deploy?"
      },
      {
       "value": "list",
       "label": "List"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per repo",
     "type": "integer",
     "default": 8,
     "min": 3,
     "max": 60,
     "group": "Layout"
    },
    {
     "key": "deploy_rule",
     "title": "Deploy window",
     "type": "choice",
     "default": "friday",
     "options": [
      {
       "value": "always",
       "label": "Any time"
      },
      {
       "value": "friday",
       "label": "Not Friday afternoon"
      },
      {
       "value": "weekend",
       "label": "Not Fri pm or weekend"
      }
     ],
     "group": "Deploy verdict"
    },
    {
     "key": "friday_hour",
     "title": "Friday cut-off (hour)",
     "type": "integer",
     "description": "No deploys from this hour on Fridays (local time)",
     "default": 15,
     "min": 0,
     "max": 23,
     "group": "Deploy verdict"
    },
    {
     "key": "refresh",
     "title": "Refresh (s)",
     "type": "integer",
     "description": "Minimum; stretched automatically to stay under GitHub's rate limit",
     "default": 120,
     "min": 60,
     "max": 3600,
     "group": "Alerts"
    },
    {
     "key": "alerts",
     "title": "Alert when a build breaks",
     "type": "toggle",
     "default": true,
     "group": "Alerts"
    }
   ]
  },
  {
   "id": "agent",
   "name": "Claude Mascot",
   "category": "productivity",
   "icon": "bot",
   "description": "Animated Claude sprite for agent status: thinking, working, waiting, done.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/agent.html",
   "preview": "media/apps/agent.gif",
   "poster": "media/apps/agent.png",
   "og_image": "media/apps/og/agent.png",
   "source": "src/deskdot/apps/agent.py",
   "actions": [],
   "settings": [
    {
     "key": "state",
     "title": "State",
     "type": "choice",
     "default": "idle",
     "options": [
      {
       "value": "idle",
       "label": "Idle"
      },
      {
       "value": "thinking",
       "label": "Thinking"
      },
      {
       "value": "working",
       "label": "Working"
      },
      {
       "value": "waiting",
       "label": "Needs input"
      },
      {
       "value": "done",
       "label": "Done"
      },
      {
       "value": "error",
       "label": "Error"
      },
      {
       "value": "sleeping",
       "label": "Sleeping"
      }
     ]
    },
    {
     "key": "label",
     "title": "Show label",
     "type": "toggle",
     "default": true
    },
    {
     "key": "color",
     "title": "Body colour",
     "type": "color",
     "default": "#f94a18"
    }
   ]
  },
  {
   "id": "custom",
   "name": "Custom",
   "category": "productivity",
   "icon": "webhook",
   "description": "Screens pushed by other tools (Home Assistant, scripts) via POST /api/custom/{name}.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/custom.html",
   "preview": "media/apps/custom.gif",
   "poster": "media/apps/custom.png",
   "og_image": "media/apps/og/custom.png",
   "source": "src/deskdot/apps/custom.py",
   "actions": [],
   "settings": [
    {
     "key": "name",
     "title": "Pushed app",
     "type": "text",
     "description": "Name used in POST /api/custom/{name}. Empty = the first pushed app.",
     "default": "",
     "max_length": 32
    }
   ]
  },
  {
   "id": "daily",
   "name": "Daily Dose",
   "category": "productivity",
   "icon": "quote",
   "description": "Quotes, dad jokes, useless facts, advice, cat facts, pun jokes and on-this-day, in rotation.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/daily.html",
   "preview": "media/apps/daily.gif",
   "poster": "media/apps/daily.png",
   "og_image": "media/apps/og/daily.png",
   "source": "src/deskdot/apps/daily.py",
   "actions": [
    {
     "id": "next",
     "label": "Next"
    }
   ],
   "settings": [
    {
     "key": "quote",
     "title": "Random quote",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "qotd",
     "title": "Quote of the day",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "dad",
     "title": "Dad joke",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "fact",
     "title": "Useless fact",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "fact_today",
     "title": "Useless fact of the day",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "advice",
     "title": "Advice",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "cat",
     "title": "Cat fact",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "joke",
     "title": "Programming & pun jokes",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "chuck",
     "title": "Chuck Norris",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "kanye",
     "title": "Kanye",
     "type": "toggle",
     "default": false,
     "group": "Sources"
    },
    {
     "key": "history",
     "title": "On this day",
     "type": "toggle",
     "default": true,
     "group": "Sources"
    },
    {
     "key": "order",
     "title": "Order",
     "type": "choice",
     "default": "cycle",
     "options": [
      {
       "value": "cycle",
       "label": "In turn"
      },
      {
       "value": "shuffle",
       "label": "Shuffle"
      }
     ],
     "group": "Sources"
    },
    {
     "key": "style",
     "title": "Style",
     "type": "choice",
     "default": "pages",
     "options": [
      {
       "value": "pages",
       "label": "Pages"
      },
      {
       "value": "lyrics",
       "label": "Spotify Lyrics"
      },
      {
       "value": "marquee",
       "label": "Marquee"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "font",
     "title": "Font",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto fit"
      },
      {
       "value": "small",
       "label": "Small"
      },
      {
       "value": "tiny",
       "label": "Tiny"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per item",
     "type": "integer",
     "description": "Minimum; long items stay until they have been read",
     "default": 14,
     "min": 5,
     "max": 180,
     "group": "Layout"
    },
    {
     "key": "pace",
     "title": "Reading pace",
     "type": "choice",
     "default": "normal",
     "options": [
      {
       "value": "slow",
       "label": "Slow"
      },
      {
       "value": "normal",
       "label": "Normal"
      },
      {
       "value": "fast",
       "label": "Fast"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "speed",
     "title": "Marquee speed (px/s)",
     "type": "integer",
     "default": 12,
     "min": 6,
     "max": 24,
     "group": "Layout"
    },
    {
     "key": "show_icon",
     "title": "Source icon",
     "type": "toggle",
     "default": true,
     "group": "Layout"
    },
    {
     "key": "show_author",
     "title": "Author line",
     "type": "toggle",
     "default": true,
     "group": "Layout"
    },
    {
     "key": "colors",
     "title": "Tag colours",
     "type": "choice",
     "default": "source",
     "options": [
      {
       "value": "source",
       "label": "Per source"
      },
      {
       "value": "custom",
       "label": "Custom accent"
      }
     ],
     "group": "Colours"
    },
    {
     "key": "accent",
     "title": "Custom accent",
     "type": "color",
     "default": "#ffaa00",
     "group": "Colours"
    },
    {
     "key": "text_color",
     "title": "Text",
     "type": "color",
     "default": "#ffffff",
     "group": "Colours"
    },
    {
     "key": "punch_color",
     "title": "Punchline",
     "type": "color",
     "default": "#ffd600",
     "group": "Colours"
    }
   ]
  },
  {
   "id": "focuspet",
   "name": "Focus Companion",
   "category": "productivity",
   "icon": "square",
   "description": "Lofi animated productivity pet that studies, takes breaks, and rests with you.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/focuspet.html",
   "preview": "media/apps/focuspet.gif",
   "poster": "media/apps/focuspet.png",
   "og_image": "media/apps/og/focuspet.png",
   "source": "src/deskdot/apps/focuspet.py",
   "actions": [],
   "settings": [
    {
     "key": "activity",
     "title": "Companion State",
     "type": "choice",
     "default": "focus",
     "options": [
      {
       "value": "focus",
       "label": "Focus / Deep Work"
      },
      {
       "value": "break",
       "label": "Coffee / Stretch Break"
      },
      {
       "value": "sleep",
       "label": "Night / Cozy Rest"
      }
     ]
    },
    {
     "key": "species",
     "title": "Pet Character",
     "type": "choice",
     "default": "bunny",
     "options": [
      {
       "value": "bunny",
       "label": "Mochi Bunny"
      },
      {
       "value": "cat",
       "label": "Ginger Cat"
      },
      {
       "value": "dog",
       "label": "Shiba Dog"
      },
      {
       "value": "fox",
       "label": "Kitsune Fox"
      },
      {
       "value": "robot",
       "label": "Byte Bot"
      }
     ]
    },
    {
     "key": "accessory",
     "title": "Head Accessory",
     "type": "choice",
     "default": "glasses",
     "options": [
      {
       "value": "none",
       "label": "No Accessory"
      },
      {
       "value": "glasses",
       "label": "Study Glasses"
      },
      {
       "value": "headphones",
       "label": "Lofi Headphones"
      },
      {
       "value": "crown",
       "label": "Focus Crown"
      }
     ]
    },
    {
     "key": "theme",
     "title": "Room Style",
     "type": "choice",
     "default": "cozy",
     "options": [
      {
       "value": "cozy",
       "label": "Cozy Warm"
      },
      {
       "value": "nordic",
       "label": "Nordic Minimal"
      },
      {
       "value": "cyber",
       "label": "Cyberpunk Neon"
      },
      {
       "value": "warm",
       "label": "Autumn Hearth"
      }
     ]
    },
    {
     "key": "timer_bar",
     "title": "Top Progress HUD",
     "type": "choice",
     "default": "on",
     "options": [
      {
       "value": "on",
       "label": "Show Progress HUD"
      },
      {
       "value": "off",
       "label": "Hide Progress HUD"
      }
     ]
    },
    {
     "key": "speed",
     "title": "Animation Speed",
     "type": "number",
     "default": 1.0,
     "min": 0.5,
     "max": 2.0
    },
    {
     "key": "brightness",
     "title": "LED Brightness",
     "type": "number",
     "default": 1.0,
     "min": 0.2,
     "max": 1.0
    }
   ]
  },
  {
   "id": "timer",
   "name": "Focus Timer",
   "category": "productivity",
   "icon": "timer",
   "description": "Pomodoro cycles, countdowns and a stopwatch. Notifies when a phase ends.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/timer.html",
   "preview": "media/apps/timer.gif",
   "poster": "media/apps/timer.png",
   "og_image": "media/apps/og/timer.png",
   "source": "src/deskdot/apps/timer.py",
   "actions": [
    {
     "id": "toggle",
     "label": "Start/Pause"
    },
    {
     "id": "reset",
     "label": "Reset"
    },
    {
     "id": "skip",
     "label": "Skip"
    }
   ],
   "settings": [
    {
     "key": "mode",
     "title": "Mode",
     "type": "choice",
     "default": "pomodoro",
     "options": [
      {
       "value": "pomodoro",
       "label": "Pomodoro"
      },
      {
       "value": "countdown",
       "label": "Countdown"
      },
      {
       "value": "stopwatch",
       "label": "Stopwatch"
      }
     ]
    },
    {
     "key": "focus",
     "title": "Focus (min)",
     "type": "integer",
     "default": 25,
     "min": 1,
     "max": 180
    },
    {
     "key": "short_break",
     "title": "Break (min)",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 60
    },
    {
     "key": "long_break",
     "title": "Long break (min)",
     "type": "integer",
     "default": 15,
     "min": 1,
     "max": 90
    },
    {
     "key": "rounds",
     "title": "Rounds before long break",
     "type": "integer",
     "default": 4,
     "min": 1,
     "max": 12
    },
    {
     "key": "countdown",
     "title": "Countdown (min)",
     "type": "integer",
     "default": 10,
     "min": 1,
     "max": 999
    },
    {
     "key": "focus_color",
     "title": "Focus colour",
     "type": "color",
     "default": "#ff1e5a"
    },
    {
     "key": "break_color",
     "title": "Break colour",
     "type": "color",
     "default": "#00ff8c"
    }
   ]
  },
  {
   "id": "habits",
   "name": "Habits",
   "category": "productivity",
   "icon": "flame",
   "description": "Up to three daily habits with a 7-day dot row, today's tick and a streak flame.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/habits.html",
   "preview": "media/apps/habits.gif",
   "poster": "media/apps/habits.png",
   "og_image": "media/apps/og/habits.png",
   "source": "src/deskdot/apps/habits.py",
   "actions": [
    {
     "id": "tick1",
     "label": "Tick 1"
    },
    {
     "id": "tick2",
     "label": "Tick 2"
    },
    {
     "id": "tick3",
     "label": "Tick 3"
    },
    {
     "id": "undo",
     "label": "Undo last"
    }
   ],
   "settings": [
    {
     "key": "habit1",
     "title": "Habit 1",
     "type": "text",
     "default": "WATER",
     "max_length": 10
    },
    {
     "key": "habit2",
     "title": "Habit 2",
     "type": "text",
     "default": "READ",
     "max_length": 10
    },
    {
     "key": "habit3",
     "title": "Habit 3",
     "type": "text",
     "description": "Leave a name empty to hide it.",
     "default": "MOVE",
     "max_length": 10
    },
    {
     "key": "color1",
     "title": "Colour 1",
     "type": "color",
     "default": "#00dcff",
     "group": "Look"
    },
    {
     "key": "color2",
     "title": "Colour 2",
     "type": "color",
     "default": "#ffd600",
     "group": "Look"
    },
    {
     "key": "color3",
     "title": "Colour 3",
     "type": "color",
     "default": "#00ff78",
     "group": "Look"
    },
    {
     "key": "streak_style",
     "title": "Streak",
     "type": "choice",
     "default": "flame",
     "options": [
      {
       "value": "flame",
       "label": "Flame + days"
      },
      {
       "value": "days",
       "label": "Days only"
      }
     ],
     "group": "Look"
    },
    {
     "key": "day_starts",
     "title": "Day starts at (hour)",
     "type": "integer",
     "description": "Ticks before this hour count for the previous day (night owls).",
     "default": 4,
     "min": 0,
     "max": 8,
     "group": "Look"
    }
   ]
  },
  {
   "id": "obs",
   "name": "OBS Status",
   "category": "productivity",
   "icon": "radio",
   "description": "LIVE/REC badges with elapsed time, current scene and dropped frames from OBS Studio.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/obs.html",
   "preview": "media/apps/obs.gif",
   "poster": "media/apps/obs.png",
   "og_image": "media/apps/og/obs.png",
   "source": "src/deskdot/apps/obs.py",
   "actions": [],
   "settings": [
    {
     "key": "host",
     "title": "Host",
     "type": "text",
     "description": "Machine running OBS",
     "default": "127.0.0.1",
     "max_length": 120,
     "group": "Connection"
    },
    {
     "key": "port",
     "title": "Port",
     "type": "integer",
     "default": 4455,
     "min": 1,
     "max": 65535,
     "group": "Connection"
    },
    {
     "key": "password",
     "title": "Password",
     "type": "text",
     "description": "OBS → Tools → WebSocket Server Settings → Show Connect Info. Never logged.",
     "default": "",
     "max_length": 120,
     "group": "Connection"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "timer",
     "options": [
      {
       "value": "timer",
       "label": "Timer hero"
      },
      {
       "value": "scene",
       "label": "Scene hero"
      }
     ],
     "group": "Display"
    },
    {
     "key": "show_drops",
     "title": "Dropped frames",
     "type": "toggle",
     "default": true,
     "group": "Display"
    },
    {
     "key": "takeover",
     "title": "Take over the playlist while live or recording",
     "type": "toggle",
     "default": true,
     "group": "Display"
    }
   ]
  },
  {
   "id": "qr",
   "name": "QR Code",
   "category": "productivity",
   "icon": "qr-code",
   "description": "Share your Wi-Fi, a link or a note as a scannable QR code (encoder written from scratch).",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/qr.html",
   "preview": "media/apps/qr.gif",
   "poster": "media/apps/qr.png",
   "og_image": "media/apps/og/qr.png",
   "source": "src/deskdot/apps/qr.py",
   "actions": [],
   "settings": [
    {
     "key": "mode",
     "title": "Content",
     "type": "choice",
     "default": "url",
     "options": [
      {
       "value": "url",
       "label": "URL"
      },
      {
       "value": "wifi",
       "label": "Wi-Fi join"
      },
      {
       "value": "text",
       "label": "Text"
      }
     ]
    },
    {
     "key": "text",
     "title": "URL / text",
     "type": "text",
     "description": "Up to 53 bytes at level L (42 at M). Shorter content gives bigger modules.",
     "default": "https://example.com",
     "max_length": 53
    },
    {
     "key": "ssid",
     "title": "Wi-Fi name (SSID)",
     "type": "text",
     "default": "MyNetwork",
     "max_length": 32,
     "group": "Wi-Fi"
    },
    {
     "key": "password",
     "title": "Wi-Fi password",
     "type": "text",
     "default": "",
     "max_length": 40,
     "group": "Wi-Fi"
    },
    {
     "key": "security",
     "title": "Security",
     "type": "choice",
     "default": "WPA",
     "options": [
      {
       "value": "WPA",
       "label": "WPA/WPA2/WPA3"
      },
      {
       "value": "WEP",
       "label": "WEP"
      },
      {
       "value": "nopass",
       "label": "Open"
      }
     ],
     "group": "Wi-Fi"
    },
    {
     "key": "hidden",
     "title": "Hidden network",
     "type": "toggle",
     "default": false,
     "group": "Wi-Fi"
    },
    {
     "key": "ecc",
     "title": "Error correction",
     "type": "choice",
     "default": "M",
     "options": [
      {
       "value": "L",
       "label": "L (7 %, more room)"
      },
      {
       "value": "M",
       "label": "M (15 %, sturdier)"
      }
     ],
     "group": "Look"
    },
    {
     "key": "dark",
     "title": "Module colour",
     "type": "color",
     "default": "#000000",
     "group": "Look"
    },
    {
     "key": "light",
     "title": "Background colour",
     "type": "color",
     "description": "Dark on light scans best.",
     "default": "#ffffff",
     "group": "Look"
    }
   ]
  },
  {
   "id": "uptime",
   "name": "Uptime Monitor",
   "category": "productivity",
   "icon": "activity",
   "description": "HTTP and TCP checks with latency sparklines, a status grid and outage alerts.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/uptime.html",
   "preview": "media/apps/uptime.gif",
   "poster": "media/apps/uptime.png",
   "og_image": "media/apps/og/uptime.png",
   "source": "src/deskdot/apps/uptime.py",
   "actions": [],
   "settings": [
    {
     "key": "targets",
     "title": "Targets",
     "type": "text",
     "description": "Comma-separated. example.com (HTTPS), http://host:8080/health, host:port or tcp://host:port (TCP connect), NAME=target to label. ICMP ping needs admin rights, so it isn't supported: use a TCP port instead.",
     "default": "google.com, github.com, 1.1.1.1:53",
     "max_length": 600,
     "group": "Targets"
    },
    {
     "key": "every",
     "title": "Check every (s)",
     "type": "integer",
     "default": 30,
     "min": 10,
     "max": 600,
     "group": "Targets"
    },
    {
     "key": "timeout",
     "title": "Timeout (s)",
     "type": "integer",
     "default": 5,
     "min": 1,
     "max": 20,
     "group": "Targets"
    },
    {
     "key": "expect",
     "title": "HTTP counts as up when",
     "type": "choice",
     "default": "ok",
     "options": [
      {
       "value": "ok",
       "label": "Status < 400"
      },
      {
       "value": "2xx",
       "label": "2xx only"
      },
      {
       "value": "any",
       "label": "Any reply < 500"
      }
     ],
     "group": "Targets"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "list",
     "options": [
      {
       "value": "list",
       "label": "List"
      },
      {
       "value": "grid",
       "label": "Grid"
      },
      {
       "value": "focus",
       "label": "Focus"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "rotate",
     "title": "Seconds per page",
     "type": "integer",
     "default": 6,
     "min": 2,
     "max": 60,
     "group": "Layout"
    },
    {
     "key": "slow_ms",
     "title": "Slow above (ms)",
     "type": "integer",
     "description": "Latency shown amber above this",
     "default": 800,
     "min": 50,
     "max": 5000,
     "group": "Layout"
    },
    {
     "key": "confirm",
     "title": "Failures before DOWN",
     "type": "integer",
     "description": "Consecutive failed checks",
     "default": 2,
     "min": 1,
     "max": 5,
     "group": "Alerts"
    },
    {
     "key": "alerts",
     "title": "Notify on down / up",
     "type": "toggle",
     "default": true,
     "group": "Alerts"
    },
    {
     "key": "flash",
     "title": "Flash on outage",
     "type": "toggle",
     "default": true,
     "group": "Alerts"
    },
    {
     "key": "takeover",
     "title": "Take over the playlist while down",
     "type": "toggle",
     "default": false,
     "group": "Alerts"
    }
   ]
  },
  {
   "id": "clock",
   "name": "Clock",
   "category": "time",
   "icon": "clock",
   "description": "Hero, stacked, minimal and analog faces with a seconds ring.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/clock.html",
   "preview": "media/apps/clock.gif",
   "poster": "media/apps/clock.png",
   "og_image": "media/apps/og/clock.png",
   "source": "src/deskdot/apps/clock.py",
   "actions": [],
   "settings": [
    {
     "key": "style",
     "title": "Style",
     "type": "choice",
     "default": "hero",
     "options": [
      {
       "value": "hero",
       "label": "Hero"
      },
      {
       "value": "stacked",
       "label": "Stacked"
      },
      {
       "value": "minimal",
       "label": "Minimal"
      },
      {
       "value": "analog",
       "label": "Analog"
      },
      {
       "value": "nixie",
       "label": "Nixie tubes"
      },
      {
       "value": "segment",
       "label": "7-segment LCD"
      },
      {
       "value": "flip",
       "label": "Split-flap"
      },
      {
       "value": "binary",
       "label": "Binary"
      },
      {
       "value": "word",
       "label": "Word clock"
      },
      {
       "value": "rings",
       "label": "Rings"
      },
      {
       "value": "sky",
       "label": "Sun & moon"
      },
      {
       "value": "rain",
       "label": "Digital rain"
      }
     ]
    },
    {
     "key": "hour24",
     "title": "24-hour",
     "type": "toggle",
     "default": true
    },
    {
     "key": "seconds",
     "title": "Seconds",
     "type": "choice",
     "default": "ring",
     "options": [
      {
       "value": "ring",
       "label": "Edge ring"
      },
      {
       "value": "none",
       "label": "Hidden"
      }
     ]
    },
    {
     "key": "blink",
     "title": "Blink colon",
     "type": "toggle",
     "default": true
    },
    {
     "key": "color",
     "title": "Time colour",
     "type": "color",
     "default": "#ffffff"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#ff4818"
    }
   ]
  },
  {
   "id": "countdown",
   "name": "Countdown",
   "category": "time",
   "icon": "hourglass",
   "description": "Counts down to any moment — days, then hours, then a live seconds clock.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/countdown.html",
   "preview": "media/apps/countdown.gif",
   "poster": "media/apps/countdown.png",
   "og_image": "media/apps/og/countdown.png",
   "source": "src/deskdot/apps/extras.py",
   "actions": [],
   "settings": [
    {
     "key": "label",
     "title": "Label",
     "type": "text",
     "default": "LAUNCH",
     "max_length": 10
    },
    {
     "key": "target",
     "title": "Date & time",
     "type": "text",
     "default": "2026-12-31T23:59:00"
    },
    {
     "key": "color",
     "title": "Colour",
     "type": "color",
     "default": "#00dcff"
    },
    {
     "key": "style",
     "title": "Style",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto (days → clock)"
      },
      {
       "value": "days",
       "label": "Days only"
      }
     ]
    }
   ]
  },
  {
   "id": "daynight",
   "name": "Day & Night",
   "category": "time",
   "icon": "earth",
   "description": "World map with the live day/night terminator, twinkling city lights, you, the sun and the moon.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/daynight.html",
   "preview": "media/apps/daynight.gif",
   "poster": "media/apps/daynight.png",
   "og_image": "media/apps/og/daynight.png",
   "source": "src/deskdot/apps/daynight.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "world",
     "options": [
      {
       "value": "world",
       "label": "World + times"
      },
      {
       "value": "half",
       "label": "Half world"
      },
      {
       "value": "globe",
       "label": "Globe"
      }
     ],
     "group": "Map"
    },
    {
     "key": "center_on_home",
     "title": "Centre on my location",
     "type": "toggle",
     "default": true,
     "group": "Map"
    },
    {
     "key": "center_lon",
     "title": "Centre longitude",
     "type": "integer",
     "description": "Used when not centred on your location.",
     "default": 0,
     "min": -180,
     "max": 180,
     "group": "Map"
    },
    {
     "key": "show_cities",
     "title": "City lights",
     "type": "toggle",
     "default": true,
     "group": "Markers"
    },
    {
     "key": "show_home",
     "title": "My location",
     "type": "toggle",
     "default": true,
     "group": "Markers"
    },
    {
     "key": "show_sun",
     "title": "Sun marker",
     "type": "toggle",
     "default": true,
     "group": "Markers"
    },
    {
     "key": "show_moon",
     "title": "Moon marker",
     "type": "toggle",
     "default": true,
     "group": "Markers"
    },
    {
     "key": "hour24",
     "title": "24-hour times",
     "type": "toggle",
     "default": true,
     "group": "Markers"
    },
    {
     "key": "day_land",
     "title": "Land (day)",
     "type": "color",
     "default": "#1e9a3c",
     "group": "Colours"
    },
    {
     "key": "day_sea",
     "title": "Sea (day)",
     "type": "color",
     "default": "#002a78",
     "group": "Colours"
    },
    {
     "key": "night_land",
     "title": "Land (night)",
     "type": "color",
     "default": "#1c2a4a",
     "group": "Colours"
    },
    {
     "key": "lights",
     "title": "City lights",
     "type": "color",
     "default": "#ffbe50",
     "group": "Colours"
    }
   ]
  },
  {
   "id": "fiveoclock",
   "name": "Five O'Clock Somewhere",
   "category": "time",
   "icon": "beer",
   "description": "A city where it's 17:xx right now (from local time-zone data), toasted with clinking glasses.",
   "kind": "clip",
   "kinds": [
    "clip"
   ],
   "page": "apps/fiveoclock.html",
   "preview": "media/apps/fiveoclock.gif",
   "poster": "media/apps/fiveoclock.png",
   "og_image": "media/apps/og/fiveoclock.png",
   "source": "src/deskdot/apps/fiveoclock.py",
   "actions": [],
   "settings": [
    {
     "key": "drink",
     "title": "Drink",
     "type": "choice",
     "default": "mixed",
     "options": [
      {
       "value": "mixed",
       "label": "Mixed"
      },
      {
       "value": "beer",
       "label": "Beer"
      },
      {
       "value": "wine",
       "label": "Wine"
      },
      {
       "value": "cocktail",
       "label": "Cocktail"
      }
     ]
    },
    {
     "key": "count",
     "title": "Cities per loop",
     "type": "integer",
     "description": "How many 17:xx cities to toast.",
     "default": 3,
     "min": 1,
     "max": 4
    },
    {
     "key": "show_country",
     "title": "Show country code",
     "type": "toggle",
     "default": false
    },
    {
     "key": "accent",
     "title": "Accent colour",
     "type": "color",
     "default": "#ffd600"
    }
   ]
  },
  {
   "id": "holidays",
   "name": "Holidays",
   "category": "time",
   "icon": "party-popper",
   "description": "Countdown to the next public holiday, a list, a month calendar and a celebration on the day.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/holidays.html",
   "preview": "media/apps/holidays.gif",
   "poster": "media/apps/holidays.png",
   "og_image": "media/apps/og/holidays.png",
   "source": "src/deskdot/apps/holidays.py",
   "actions": [],
   "settings": [
    {
     "key": "country",
     "title": "Country code",
     "type": "text",
     "description": "ISO code such as IN, US, DE. Blank = the country of your location",
     "default": "",
     "max_length": 2,
     "group": "Data"
    },
    {
     "key": "observances",
     "title": "Include observances",
     "type": "toggle",
     "description": "Also show observances and regional-only days",
     "default": false,
     "group": "Data"
    },
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "countdown",
     "options": [
      {
       "value": "countdown",
       "label": "Countdown"
      },
      {
       "value": "list",
       "label": "List"
      },
      {
       "value": "calendar",
       "label": "Month calendar"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "count",
     "title": "Holidays in the list",
     "type": "integer",
     "default": 4,
     "min": 1,
     "max": 8,
     "group": "Layout"
    },
    {
     "key": "celebrate",
     "title": "Celebrate on the day",
     "type": "toggle",
     "default": true,
     "group": "Layout"
    },
    {
     "key": "icon",
     "title": "Festive icon",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "Auto (by holiday)"
      },
      {
       "value": "fireworks",
       "label": "Fireworks"
      },
      {
       "value": "confetti",
       "label": "Confetti"
      },
      {
       "value": "diya",
       "label": "Lamp"
      },
      {
       "value": "star",
       "label": "Star"
      },
      {
       "value": "none",
       "label": "None"
      }
     ],
     "group": "Colours"
    },
    {
     "key": "accent",
     "title": "Accent",
     "type": "color",
     "default": "#ffaa00",
     "group": "Colours"
    },
    {
     "key": "text_color",
     "title": "Name colour",
     "type": "color",
     "default": "#ffffff",
     "group": "Colours"
    }
   ]
  },
  {
   "id": "progress",
   "name": "Progress",
   "category": "time",
   "icon": "hourglass",
   "description": "Day, week, month, year, life and custom-event progress as bars, a big ring or a dots grid.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/progress.html",
   "preview": "media/apps/progress.gif",
   "poster": "media/apps/progress.png",
   "og_image": "media/apps/og/progress.png",
   "source": "src/deskdot/apps/progress.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "bars",
     "options": [
      {
       "value": "bars",
       "label": "Stacked bars"
      },
      {
       "value": "ring",
       "label": "Big ring"
      },
      {
       "value": "dots",
       "label": "Dots grid"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "focus",
     "title": "Ring / dots show",
     "type": "choice",
     "default": "year",
     "options": [
      {
       "value": "day",
       "label": "Day"
      },
      {
       "value": "week",
       "label": "Week"
      },
      {
       "value": "month",
       "label": "Month"
      },
      {
       "value": "year",
       "label": "Year"
      },
      {
       "value": "life",
       "label": "Life"
      },
      {
       "value": "event1",
       "label": "Event 1"
      },
      {
       "value": "event2",
       "label": "Event 2"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "decimals",
     "title": "Decimals",
     "type": "integer",
     "default": 1,
     "min": 0,
     "max": 2,
     "group": "Layout"
    },
    {
     "key": "page_seconds",
     "title": "Seconds per page",
     "type": "integer",
     "default": 6,
     "min": 2,
     "max": 60,
     "group": "Layout"
    },
    {
     "key": "show_day",
     "title": "Day",
     "type": "toggle",
     "default": true,
     "group": "Bars"
    },
    {
     "key": "show_week",
     "title": "Week",
     "type": "toggle",
     "default": true,
     "group": "Bars"
    },
    {
     "key": "show_month",
     "title": "Month",
     "type": "toggle",
     "default": true,
     "group": "Bars"
    },
    {
     "key": "show_year",
     "title": "Year",
     "type": "toggle",
     "default": true,
     "group": "Bars"
    },
    {
     "key": "show_life",
     "title": "Life",
     "type": "toggle",
     "default": false,
     "group": "Bars"
    },
    {
     "key": "show_events",
     "title": "Custom events",
     "type": "toggle",
     "default": true,
     "group": "Bars"
    },
    {
     "key": "week_start",
     "title": "Week starts",
     "type": "choice",
     "default": "mon",
     "options": [
      {
       "value": "mon",
       "label": "Monday"
      },
      {
       "value": "sun",
       "label": "Sunday"
      }
     ],
     "group": "Bars"
    },
    {
     "key": "birth_date",
     "title": "Birth date",
     "type": "text",
     "default": "1990-01-01",
     "group": "Life"
    },
    {
     "key": "life_years",
     "title": "Life expectancy (years)",
     "type": "integer",
     "default": 80,
     "min": 30,
     "max": 120,
     "group": "Life"
    },
    {
     "key": "event1_on",
     "title": "Event 1 on",
     "type": "toggle",
     "default": false,
     "group": "Events"
    },
    {
     "key": "event1_label",
     "title": "Event 1 label",
     "type": "text",
     "default": "PROJECT",
     "max_length": 12,
     "group": "Events"
    },
    {
     "key": "event1_start",
     "title": "Event 1 start",
     "type": "text",
     "default": "2026-07-01T00:00:00",
     "group": "Events"
    },
    {
     "key": "event1_end",
     "title": "Event 1 end",
     "type": "text",
     "default": "2026-12-31T18:00:00",
     "group": "Events"
    },
    {
     "key": "event2_on",
     "title": "Event 2 on",
     "type": "toggle",
     "default": false,
     "group": "Events"
    },
    {
     "key": "event2_label",
     "title": "Event 2 label",
     "type": "text",
     "default": "TRIP",
     "max_length": 12,
     "group": "Events"
    },
    {
     "key": "event2_start",
     "title": "Event 2 start",
     "type": "text",
     "default": "2026-09-01T00:00:00",
     "group": "Events"
    },
    {
     "key": "event2_end",
     "title": "Event 2 end",
     "type": "text",
     "default": "2026-10-15T00:00:00",
     "group": "Events"
    }
   ]
  },
  {
   "id": "sky",
   "name": "Sun & Moon",
   "category": "time",
   "icon": "sunrise",
   "description": "Sunrise, sunset and the sun's path today, golden and blue hour, and the moon's phase.",
   "kind": "stream",
   "kinds": [
    "stream"
   ],
   "page": "apps/sky.html",
   "preview": "media/apps/sky.gif",
   "poster": "media/apps/sky.png",
   "og_image": "media/apps/og/sky.png",
   "source": "src/deskdot/apps/sky.py",
   "actions": [],
   "settings": [
    {
     "key": "layout",
     "title": "Layout",
     "type": "choice",
     "default": "arc",
     "options": [
      {
       "value": "arc",
       "label": "Sun arc"
      },
      {
       "value": "moon",
       "label": "Moon"
      },
      {
       "value": "times",
       "label": "Sun times"
      },
      {
       "value": "combined",
       "label": "Combined"
      }
     ],
     "group": "Layout"
    },
    {
     "key": "hour24",
     "title": "24-hour times",
     "type": "toggle",
     "default": true,
     "group": "Layout"
    },
    {
     "key": "stars",
     "title": "Stars after dark",
     "type": "toggle",
     "default": true,
     "group": "Layout"
    },
    {
     "key": "page_seconds",
     "title": "Seconds per page",
     "type": "integer",
     "default": 5,
     "min": 2,
     "max": 30,
     "group": "Layout"
    },
    {
     "key": "hemisphere",
     "title": "Moon orientation",
     "type": "choice",
     "default": "auto",
     "options": [
      {
       "value": "auto",
       "label": "From location"
      },
      {
       "value": "north",
       "label": "Northern"
      },
      {
       "value": "south",
       "label": "Southern"
      }
     ],
     "group": "Moon"
    }
   ]
  }
 ]
}
