feat: challenge choice icons and decisions tracker in sidebar

- 20 choice icons generated (one per A/B/C option across 7 challenges)
- Challenge buttons now card-style columns with icon, key badge overlay, and text
- Decisions sidebar section shows icon grid of choices made so far
- Slots colored by rating (green/yellow/red), hover label shows details
- Unified icon generation script handles both artifact and choice prompts
- Choice icon prompt reference JSON

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alexander Brown
2026-03-24 12:47:30 -07:00
committed by DrJKL
parent 629513579e
commit 0006815ebf
23 changed files with 293 additions and 58 deletions

View File

@@ -0,0 +1,90 @@
{
"meta": {
"style": "Pixel art icon, 128x128, dark background, game UI button icon style, clean readable silhouette",
"usage": "Each key is {room}-{choiceKey lowercase}. Used in challenge choice buttons in adventure.html.",
"model": "Z-Image Turbo (no LoRA)",
"resolution": "128x128"
},
"choices": {
"components-a": {
"label": "Composition over inheritance",
"prompt": "Pixel art icon of puzzle pieces snapping together cleanly, green glow, dark background, game UI icon"
},
"components-b": {
"label": "Barrel file reordering",
"prompt": "Pixel art icon of a stack of files being shuffled with arrows, amber warning glow, dark background, game UI icon"
},
"components-c": {
"label": "Factory injection",
"prompt": "Pixel art icon of a factory building with a syringe injecting into it, blue mechanical glow, dark background, game UI icon"
},
"stores-a": {
"label": "Centralize into graph.incrementVersion()",
"prompt": "Pixel art icon of scattered dots converging into a single glowing funnel point, green glow, dark background, game UI icon"
},
"stores-b": {
"label": "Add a JavaScript Proxy",
"prompt": "Pixel art icon of a shield proxy intercepting arrows mid-flight, amber translucent glow, dark background, game UI icon"
},
"stores-c": {
"label": "Leave it as-is",
"prompt": "Pixel art icon of a shrug gesture with cobwebs on old machinery, grey muted glow, dark background, game UI icon"
},
"services-a": {
"label": "5-phase incremental plan",
"prompt": "Pixel art icon of five stepping stones ascending in a staircase with checkmarks, green glow, dark background, game UI icon"
},
"services-b": {
"label": "Big bang rewrite",
"prompt": "Pixel art icon of a dynamite stick with lit fuse and explosion sparks, red danger glow, dark background, game UI icon"
},
"services-c": {
"label": "Strangler fig pattern",
"prompt": "Pixel art icon of vines growing around and enveloping an old tree trunk, green and brown organic glow, dark background, game UI icon"
},
"litegraph-a": {
"label": "Rewrite from scratch",
"prompt": "Pixel art icon of a wrecking ball demolishing a building into rubble, red destructive glow, dark background, game UI icon"
},
"litegraph-b": {
"label": "Extract incrementally",
"prompt": "Pixel art icon of surgical tweezers carefully extracting a glowing module from a larger block, green precise glow, dark background, game UI icon"
},
"litegraph-c": {
"label": "Add a facade layer",
"prompt": "Pixel art icon of a decorative mask covering a cracked wall, yellow cosmetic glow, dark background, game UI icon"
},
"ecs-a": {
"label": "Branded types with cast helpers",
"prompt": "Pixel art icon of ID badges with distinct colored stamps and a compiler checkmark, green type-safe glow, dark background, game UI icon"
},
"ecs-b": {
"label": "String prefixes at runtime",
"prompt": "Pixel art icon of text labels being parsed with a magnifying glass at runtime, amber slow glow, dark background, game UI icon"
},
"ecs-c": {
"label": "Keep plain numbers",
"prompt": "Pixel art icon of bare numbers floating unprotected with a question mark, red risky glow, dark background, game UI icon"
},
"renderer-a": {
"label": "Separate update and render phases",
"prompt": "Pixel art icon of two clean pipeline stages labeled U and R with an arrow between them, green orderly glow, dark background, game UI icon"
},
"renderer-b": {
"label": "Dirty flags and deferred render",
"prompt": "Pixel art icon of a flag with a smudge mark and a clock showing delay, amber patch glow, dark background, game UI icon"
},
"composables-a": {
"label": "Y.js CRDTs",
"prompt": "Pixel art icon of two documents merging seamlessly with sync arrows and no conflicts, green collaboration glow, dark background, game UI icon"
},
"composables-b": {
"label": "Polling-based sync",
"prompt": "Pixel art icon of a clock with circular refresh arrows and flickering signal, red laggy glow, dark background, game UI icon"
},
"composables-c": {
"label": "Skip collaboration for now",
"prompt": "Pixel art icon of a single person at a desk with a pause symbol, grey neutral glow, dark background, game UI icon"
}
}
}

View File

@@ -313,47 +313,73 @@
#challenge-choices {
padding: 8px 16px 16px;
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
}
.challenge-choice-btn {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
border-radius: 10px;
padding: 0;
color: var(--text);
font-family: inherit;
font-size: 13px;
cursor: pointer;
text-align: left;
text-align: center;
transition: all 0.15s;
display: flex;
align-items: flex-start;
gap: 10px;
flex-direction: column;
overflow: hidden;
}
.challenge-choice-btn:hover {
border-color: var(--yellow);
background: rgba(210, 153, 34, 0.08);
transform: translateY(-2px);
}
.challenge-choice-btn .choice-icon-wrap {
position: relative;
background: var(--bg);
padding: 12px;
}
.challenge-choice-btn .choice-key {
position: absolute;
top: 6px;
left: 6px;
background: var(--yellow);
color: var(--bg);
padding: 2px 8px;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
font-size: 10px;
font-weight: 700;
min-width: 24px;
text-align: center;
flex-shrink: 0;
margin-top: 1px;
line-height: 1.4;
}
.challenge-choice-btn .choice-text { flex: 1; }
.challenge-choice-btn .choice-label { display: block; }
.challenge-choice-btn .choice-hint { display: block; font-size: 11px; color: var(--muted); margin-top: 2px; }
.challenge-choice-btn .choice-icon {
width: 56px;
height: 56px;
border-radius: 8px;
border: 1px solid var(--border);
overflow: hidden;
margin: 0 auto;
}
.challenge-choice-btn .choice-icon img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.challenge-choice-btn .choice-text {
padding: 10px 12px 14px;
}
.challenge-choice-btn .choice-label { display: block; font-weight: 600; font-size: 12px; }
.challenge-choice-btn .choice-hint { display: block; font-size: 10px; color: var(--muted); margin-top: 4px; line-height: 1.4; }
/* --- Result Banner --- */
#result-banner {
@@ -729,6 +755,57 @@
.scorecard-row .sc-yours.miss { color: var(--yellow); }
.scorecard-row .sc-best { color: var(--accent); font-size: 11px; min-width: 60px; }
/* --- Decisions grid --- */
#dec-label {
padding: 4px 12px;
font-size: 11px;
color: var(--accent);
min-height: 22px;
border-bottom: 1px solid var(--border);
user-select: none;
}
#dec-label .dec-label-type {
color: var(--muted);
font-size: 9px;
text-transform: uppercase;
margin-left: 6px;
}
.dec-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
gap: 6px;
padding: 8px;
}
.dec-slot {
aspect-ratio: 1;
border-radius: 6px;
background: var(--bg);
border: 2px solid var(--border);
overflow: hidden;
cursor: default;
position: relative;
transition: border-color 0.15s;
}
.dec-slot:hover { border-color: var(--accent); }
.dec-slot.good { border-color: var(--green); }
.dec-slot.ok { border-color: var(--yellow); }
.dec-slot.bad { border-color: var(--red); }
.dec-slot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dec-slot.empty {
border-style: dashed;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: var(--border);
}
/* --- Room transition --- */
#narrative { transition: opacity 0.15s ease; }
#narrative.transitioning { opacity: 0.4; }
@@ -839,6 +916,16 @@
<div class="log-entry" style="color:var(--muted)">Empty &mdash; explore to find artifacts.</div>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-header">
<span>Decisions</span>
<span id="dec-count">0/7</span>
</div>
<div id="dec-label">&nbsp;</div>
<div class="sidebar-body" id="decisions">
<div class="log-entry" style="color:var(--muted)">No decisions yet.</div>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-header">
<span>Log</span>
@@ -946,21 +1033,21 @@
`,
choices: [
{
key: 'A', label: 'Composition over inheritance',
key: 'A', label: 'Composition over inheritance', icon: 'components-a',
hint: 'Subgraph HAS a graph, not IS a graph. ECS eliminates class inheritance entirely.',
effects: { techDebt: -10, quality: 15, morale: 5, migrationProgress: 1 },
rating: 'good',
feedback: 'The circular dependency dissolves. Subgraph becomes a thin wrapper holding a graph reference. Components replace inheritance.',
},
{
key: 'B', label: 'Barrel file reordering',
key: 'B', label: 'Barrel file reordering', icon: 'components-b',
hint: 'Rearrange exports so the cycle resolves at module load time.',
effects: { techDebt: 10, quality: -5, morale: -5 },
rating: 'bad',
feedback: 'The imports stop crashing... for now. But the underlying coupling remains, and any new file touching both classes risks reviving the cycle.',
},
{
key: 'C', label: 'Factory injection',
key: 'C', label: 'Factory injection', icon: 'components-c',
hint: 'Pass a graph factory function to break the static import cycle.',
effects: { techDebt: -5, quality: 10 },
rating: 'ok',
@@ -1003,21 +1090,21 @@
`,
choices: [
{
key: 'A', label: 'Centralize into graph.incrementVersion()',
key: 'A', label: 'Centralize into graph.incrementVersion()', icon: 'stores-a',
hint: 'Route all 19 sites through a single method. Phase 0a of the migration plan.',
effects: { techDebt: -15, quality: 15, migrationProgress: 1 },
rating: 'good',
feedback: 'All 19 scattered increments now flow through one method. Change tracking becomes auditable, and the VersionSystem has a single hook point.',
},
{
key: 'B', label: 'Add a JavaScript Proxy',
key: 'B', label: 'Add a JavaScript Proxy', icon: 'stores-b',
hint: 'Intercept all writes to _version automatically.',
effects: { techDebt: 5, quality: 5, morale: -5 },
rating: 'ok',
feedback: 'The Proxy catches mutations, but adds runtime overhead and makes debugging opaque. The scattered sites remain in the code.',
},
{
key: 'C', label: 'Leave it as-is',
key: 'C', label: 'Leave it as-is', icon: 'stores-c',
hint: 'It works. Don\'t touch it.',
effects: { techDebt: 10, morale: 5 },
rating: 'bad',
@@ -1058,21 +1145,21 @@
`,
choices: [
{
key: 'A', label: '5-phase incremental plan',
key: 'A', label: '5-phase incremental plan', icon: 'services-a',
hint: 'Foundation \u2192 Types \u2192 Bridge \u2192 Systems \u2192 Legacy Removal. Each phase is independently shippable.',
effects: { quality: 15, morale: 10, migrationProgress: 1 },
rating: 'good',
feedback: 'The team maps out five phases, each independently testable and shippable. Old and new coexist during transition. Production never breaks.',
},
{
key: 'B', label: 'Big bang rewrite',
key: 'B', label: 'Big bang rewrite', icon: 'services-b',
hint: 'Freeze features, rewrite everything in parallel, swap when ready.',
effects: { techDebt: -10, quality: 5, morale: -20 },
rating: 'bad',
feedback: 'Feature freeze begins. Weeks pass. The rewrite grows scope. Morale plummets. The old codebase drifts further from the new one.',
},
{
key: 'C', label: 'Strangler fig pattern',
key: 'C', label: 'Strangler fig pattern', icon: 'services-c',
hint: 'Build new ECS beside old code, migrate consumers one by one.',
effects: { quality: 10, morale: 5 },
rating: 'ok',
@@ -1114,21 +1201,21 @@
`,
choices: [
{
key: 'A', label: 'Rewrite from scratch',
key: 'A', label: 'Rewrite from scratch', icon: 'litegraph-a',
hint: 'Tear it all down and rebuild with clean architecture from day one.',
effects: { techDebt: -20, quality: 5, morale: -25 },
rating: 'bad',
feedback: 'The rewrite begins heroically... and stalls at month three. The team burns out reimplementing edge cases the god objects handled implicitly.',
},
{
key: 'B', label: 'Extract incrementally',
key: 'B', label: 'Extract incrementally', icon: 'litegraph-b',
hint: 'Peel responsibilities into focused modules one at a time. Position first, then connectivity, then rendering.',
effects: { techDebt: -10, quality: 15, morale: 5, migrationProgress: 1 },
rating: 'good',
feedback: 'Position extraction lands first (it\'s already in LayoutStore). Then connectivity. Each extraction is a small, testable PR. The god objects shrink steadily.',
},
{
key: 'C', label: 'Add a facade layer',
key: 'C', label: 'Add a facade layer', icon: 'litegraph-c',
hint: 'Wrap the god objects with a clean API without changing internals.',
effects: { techDebt: 5, quality: 5, morale: 10 },
rating: 'ok',
@@ -1172,21 +1259,21 @@
`,
choices: [
{
key: 'A', label: 'Branded types with cast helpers',
key: 'A', label: 'Branded types with cast helpers', icon: 'ecs-a',
hint: 'type NodeEntityId = number & { __brand: \'NodeEntityId\' } — compile-time safety, zero runtime cost.',
effects: { techDebt: -15, quality: 20, migrationProgress: 1 },
rating: 'good',
feedback: 'The compiler now catches cross-kind ID bugs. Cast helpers at system boundaries (asNodeEntityId()) keep the ergonomics clean. Phase 1a complete.',
},
{
key: 'B', label: 'String prefixes at runtime',
key: 'B', label: 'String prefixes at runtime', icon: 'ecs-b',
hint: '"node:42", "link:7" — parse and validate at every usage site.',
effects: { techDebt: 5, quality: 5, morale: -5 },
rating: 'ok',
feedback: 'Runtime checks catch some bugs, but parsing overhead spreads everywhere. And someone will forget the prefix check in a hot path.',
},
{
key: 'C', label: 'Keep plain numbers',
key: 'C', label: 'Keep plain numbers', icon: 'ecs-c',
hint: 'Just be careful. Document which IDs are which.',
effects: { techDebt: 15, quality: -5 },
rating: 'bad',
@@ -1229,14 +1316,14 @@
`,
choices: [
{
key: 'A', label: 'Separate update and render phases',
key: 'A', label: 'Separate update and render phases', icon: 'renderer-a',
hint: 'Compute all layout in an update pass, then render as a pure read-only pass. Matches the ECS system pipeline.',
effects: { techDebt: -15, quality: 15, migrationProgress: 1 },
rating: 'good',
feedback: 'The pipeline becomes: Input \u2192 Update (layout, connectivity) \u2192 Render (read-only). Draw order no longer matters. Bugs vanish.',
},
{
key: 'B', label: 'Dirty flags and deferred render',
key: 'B', label: 'Dirty flags and deferred render', icon: 'renderer-b',
hint: 'Mark mutated nodes dirty, skip them, re-render next frame.',
effects: { techDebt: -5, quality: 5, morale: 5 },
rating: 'ok',
@@ -1275,21 +1362,21 @@
`,
choices: [
{
key: 'A', label: 'Y.js CRDTs',
key: 'A', label: 'Y.js CRDTs', icon: 'composables-a',
hint: 'Conflict-free replicated data types. Merge without coordination. Already proven at scale.',
effects: { techDebt: -10, quality: 15, morale: 10 },
rating: 'good',
feedback: 'Y.js CRDT maps back the layout store. Concurrent edits merge automatically. ADR 0003 is realized. The collaboration future is here.',
},
{
key: 'B', label: 'Polling-based sync',
key: 'B', label: 'Polling-based sync', icon: 'composables-b',
hint: 'Fetch full state every few seconds, merge manually, hope for the best.',
effects: { techDebt: 10, quality: -5, morale: -5 },
rating: 'bad',
feedback: 'Polling creates a flickering, laggy experience. Two users move the same node and one edit is silently lost. Support tickets pile up.',
},
{
key: 'C', label: 'Skip collaboration for now',
key: 'C', label: 'Skip collaboration for now', icon: 'composables-c',
hint: 'Single-user editing only. Focus on other priorities.',
effects: { morale: 5 },
rating: 'ok',
@@ -1392,6 +1479,9 @@
inventory: $('#inventory'),
invCount: $('#inv-count'),
invLabel: $('#inv-label'),
decisions: $('#decisions'),
decCount: $('#dec-count'),
decLabel: $('#dec-label'),
log: $('#log'),
logCount: $('#log-count'),
barDebt: $('#bar-debt'),
@@ -1554,6 +1644,7 @@
// Update HUD
renderStats()
renderInventory()
renderDecisions()
renderMap()
addLog(`Entered: ${room.title}`)
@@ -1564,15 +1655,21 @@
els.challengeTitle.textContent = challenge.title
els.challengeDesc.innerHTML = challenge.description.trim()
els.challengeChoices.innerHTML = challenge.choices.map(c => `
<button class="challenge-choice-btn" data-key="${c.key}">
<span class="choice-key">${c.key}</span>
els.challengeChoices.innerHTML = challenge.choices.map(c => {
const iconImg = c.icon
? `<div class="choice-icon"><img src="icons/${c.icon}.png" alt="" onerror="this.parentElement.style.display='none'"></div>`
: ''
return `<button class="challenge-choice-btn" data-key="${c.key}">
<div class="choice-icon-wrap">
<span class="choice-key">${c.key}</span>
${iconImg}
</div>
<div class="choice-text">
<span class="choice-label">${c.label}</span>
<span class="choice-hint">${c.hint}</span>
</div>
</button>
`).join('')
</button>`
}).join('')
els.challengeChoices.querySelectorAll('.challenge-choice-btn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -1629,6 +1726,7 @@
// Show navigation choices
renderChoices(room.choices)
renderDecisions()
// Log
addLog(`Challenge resolved: ${challenge.title}`, choice.rating === 'good' ? 'discovery' : choice.rating === 'bad' ? 'error' : 'warning')
@@ -1684,6 +1782,42 @@
els.invCount.textContent = state.inventory.length
}
function renderDecisions() {
const challengeRooms = Object.entries(rooms).filter(([, r]) => r.challenge)
const made = Object.keys(state.challengeChoices).length
if (made > 0) {
els.decisions.innerHTML = '<div class="dec-grid">' + challengeRooms.map(([roomId, room]) => {
const choiceKey = state.challengeChoices[roomId]
if (!choiceKey) {
return `<div class="dec-slot empty">?</div>`
}
const choice = room.challenge.choices.find(c => c.key === choiceKey)
const iconImg = choice.icon
? `<img src="icons/${choice.icon}.png" alt="${choice.label}">`
: `<span style="font-size:18px;color:var(--text);display:flex;align-items:center;justify-content:center;height:100%">${choiceKey}</span>`
return `<div class="dec-slot ${choice.rating}" data-dec-room="${roomId}">${iconImg}</div>`
}).join('') + '</div>'
els.decisions.querySelectorAll('.dec-slot[data-dec-room]').forEach(slot => {
const roomId = slot.dataset.decRoom
const room = rooms[roomId]
const choiceKey = state.challengeChoices[roomId]
const choice = room.challenge.choices.find(c => c.key === choiceKey)
slot.addEventListener('mouseenter', () => {
els.decLabel.innerHTML = `${choice.label}<span class="dec-label-type">${room.challenge.title}</span>`
})
slot.addEventListener('mouseleave', () => {
els.decLabel.innerHTML = '&nbsp;'
})
})
} else {
els.decisions.innerHTML = '<div class="log-entry" style="color:var(--muted)">No decisions yet.</div>'
}
els.decLabel.innerHTML = '&nbsp;'
els.decCount.textContent = made + '/' + TOTAL_CHALLENGES
}
function addLog(message, type = '') {
state.log.unshift({ message, type })
els.log.innerHTML = state.log.map(l =>

View File

@@ -14,7 +14,8 @@ import urllib.error
COMFY_URL = "http://localhost:8188"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROMPTS_FILE = os.path.join(SCRIPT_DIR, "adventure-icon-prompts.json")
ARTIFACT_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-icon-prompts.json")
CHOICE_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-choice-icon-prompts.json")
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "icons")
BASE_SEED = 7777
WIDTH = 128
@@ -120,20 +121,30 @@ def download_image(filename, subfolder, dest_path):
def main():
with open(PROMPTS_FILE) as f:
data = json.load(f)
artifacts = data["artifacts"]
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Collect all icons from both prompt files
all_icons = {}
with open(ARTIFACT_PROMPTS) as f:
data = json.load(f)
for icon_id, entry in data["artifacts"].items():
all_icons[icon_id] = entry["prompt"]
if os.path.exists(CHOICE_PROMPTS):
with open(CHOICE_PROMPTS) as f:
data = json.load(f)
for icon_id, entry in data["choices"].items():
all_icons[icon_id] = entry["prompt"]
# Filter out already-generated icons
to_generate = {}
for artifact_id, artifact in artifacts.items():
dest = os.path.join(OUTPUT_DIR, f"{artifact_id}.png")
for icon_id, prompt in all_icons.items():
dest = os.path.join(OUTPUT_DIR, f"{icon_id}.png")
if os.path.exists(dest):
print(f" Skipping {artifact_id}.png (already exists)")
print(f" Skipping {icon_id}.png (already exists)")
else:
to_generate[artifact_id] = artifact
to_generate[icon_id] = prompt
if not to_generate:
print("All icons already generated. Nothing to do.")
@@ -141,20 +152,20 @@ def main():
# Submit jobs
jobs = []
for i, (artifact_id, artifact) in enumerate(to_generate.items()):
prefix = f"adventure-icons/{artifact_id}"
wf = build_workflow(artifact["prompt"], BASE_SEED + i, prefix)
for i, (icon_id, prompt) in enumerate(to_generate.items()):
prefix = f"adventure-icons/{icon_id}"
wf = build_workflow(prompt, BASE_SEED + i, prefix)
result = submit_prompt(wf)
prompt_id = result["prompt_id"]
jobs.append((artifact_id, prompt_id))
print(f" Submitted: {artifact_id} -> {prompt_id}")
jobs.append((icon_id, prompt_id))
print(f" Submitted: {icon_id} -> {prompt_id}")
print(f"\n{len(jobs)} jobs queued. Polling for completion...\n")
# Poll for completion
completed = set()
while len(completed) < len(jobs):
for artifact_id, prompt_id in jobs:
for icon_id, prompt_id in jobs:
if prompt_id in completed:
continue
history = poll_history(prompt_id, timeout=5)
@@ -165,9 +176,9 @@ def main():
for img in node_out.get("images", []):
src_filename = img["filename"]
subfolder = img.get("subfolder", "")
dest = os.path.join(OUTPUT_DIR, f"{artifact_id}.png")
dest = os.path.join(OUTPUT_DIR, f"{icon_id}.png")
download_image(src_filename, subfolder, dest)
print(f" [{len(completed)}/{len(jobs)}] {artifact_id}.png downloaded")
print(f" [{len(completed)}/{len(jobs)}] {icon_id}.png downloaded")
if len(completed) < len(jobs):
time.sleep(2)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB