chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled

Web app: new entities (Image, RenderedAsset, SharedImage, Token,
DeviceImageHistory), enums, repositories, controllers, message handlers,
migrations, tests, frontend upload/library/sticker UI, Vue components.

Firmware: EPD background screen binaries + gen scripts, setup_bg header.

Infra: ddev config, test bundle, gitignore coverage dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:11:31 -04:00
parent dd0970ed7c
commit 4002ff9fbf
156 changed files with 27333 additions and 92 deletions
+820
View File
@@ -0,0 +1,820 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=800">
<title>AP Provisioning Screen — pictureFrame mockup</title>
<style>
/*
HARDWARE CONSTRAINTS: 800×480px. Six colors only.
#1a1a1a BLACK
#f5f5f0 WHITE
#f0d000 YELLOW
#c03020 RED
#1840c0 BLUE
#10a040 GREEN
No gradients. No drop shadows. No anti-aliasing. No other colors.
No fonts below ~16px.
This file is a design mockup — open at 100% zoom in browser.
*/
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #888;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Courier New', Courier, monospace;
}
/* Outer frame — the physical bezel of the device */
.device-bezel {
width: 840px;
height: 520px;
background: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* The e-ink display surface — exactly 800×480 */
.display {
width: 800px;
height: 480px;
background: #f5f5f0;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
YELLOW STATUS BAR — signals AP mode / action required
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
.status-bar {
width: 800px;
height: 52px;
background: #f0d000;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
flex-shrink: 0;
}
.status-bar-label {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #1a1a1a;
}
.status-bar-network {
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
font-weight: 700;
color: #1a1a1a;
letter-spacing: 0.06em;
}
/* Monospaced network name chip */
.network-chip {
display: inline-block;
background: #1a1a1a;
color: #f0d000;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
padding: 4px 10px;
}
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MAIN BODY — three-column layout
LEFT: header + instructions
CENTER: orientation diagrams
RIGHT: QR code
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
.body {
flex: 1;
display: flex;
flex-direction: row;
}
/* ── LEFT PANEL ───────────────────────────────────── */
.panel-left {
width: 310px;
flex-shrink: 0;
padding: 24px 20px 20px 28px;
display: flex;
flex-direction: column;
justify-content: space-between;
border-right: 2px solid #1a1a1a;
}
.main-heading {
font-family: 'Courier New', Courier, monospace;
font-size: 26px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.2;
letter-spacing: -0.01em;
}
.main-heading em {
font-style: normal;
color: #1a1a1a;
/* Underline in yellow: simulate with border since no CSS effects */
border-bottom: 3px solid #f0d000;
}
.step-list {
list-style: none;
margin-top: 18px;
display: flex;
flex-direction: column;
gap: 12px;
}
.step-item {
display: flex;
align-items: flex-start;
gap: 10px;
}
.step-num {
width: 24px;
height: 24px;
background: #1a1a1a;
color: #f0d000;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.step-text {
font-family: 'Courier New', Courier, monospace;
font-size: 15px;
color: #1a1a1a;
line-height: 1.35;
padding-top: 3px;
}
.step-text strong {
font-weight: 700;
}
/* Divider */
.left-divider {
height: 2px;
background: #1a1a1a;
margin: 16px 0 14px;
}
.footnote {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
color: #1a1a1a;
line-height: 1.4;
opacity: 0.7;
}
/* ── CENTER PANEL — orientation diagrams ──────────── */
.panel-center {
width: 196px;
flex-shrink: 0;
border-right: 2px solid #1a1a1a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 16px 12px;
}
.orient-label {
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #1a1a1a;
text-align: center;
margin-bottom: 6px;
}
.orient-block {
display: flex;
flex-direction: column;
align-items: center;
}
/* Landscape: wide rect + bottom ribbon */
.orient-landscape-frame {
width: 110px;
height: 66px;
border: 3px solid #1a1a1a;
background: #f5f5f0;
position: relative;
}
/* Corner tick marks to suggest the physical frame */
.orient-landscape-frame::before,
.orient-landscape-frame::after {
content: '';
position: absolute;
background: #1a1a1a;
}
.orient-landscape-ribbon {
width: 110px;
height: 10px;
background: #1a1a1a;
/* The power ribbon / cable notch at bottom */
}
/* Portrait: tall rect + left ribbon */
.orient-portrait-frame {
width: 64px;
height: 106px;
border: 3px solid #1a1a1a;
background: #f5f5f0;
display: flex;
align-items: flex-end;
justify-content: flex-end;
position: relative;
}
.orient-portrait-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.orient-portrait-ribbon {
width: 10px;
height: 106px;
background: #1a1a1a;
}
/* Active/current orientation highlight */
.orient-active .orient-landscape-frame,
.orient-active .orient-portrait-frame {
border-color: #f0d000;
border-width: 3px;
}
.orient-active .orient-landscape-ribbon,
.orient-active .orient-portrait-ribbon {
background: #f0d000;
}
.orient-active .orient-label {
color: #1a1a1a;
}
/* Tiny check mark for active orientation */
.active-badge {
width: 18px;
height: 18px;
background: #f0d000;
border: 2px solid #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 900;
color: #1a1a1a;
margin-top: 5px;
}
.orient-divider {
width: 100%;
height: 1px;
background: #1a1a1a;
opacity: 0.3;
}
.orient-section-title {
font-family: 'Courier New', Courier, monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.25em;
text-transform: uppercase;
color: #1a1a1a;
opacity: 0.55;
text-align: center;
margin-bottom: 10px;
}
/* ── RIGHT PANEL — QR code ────────────────────────── */
.panel-right {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
gap: 10px;
}
.qr-instruction {
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #1a1a1a;
text-align: center;
}
.qr-wrapper {
width: 196px;
height: 196px;
background: #f5f5f0;
border: 3px solid #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* Yellow corner brackets — frame the QR */
.qr-wrapper::before {
content: '';
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border: 3px solid #f0d000;
pointer-events: none;
}
.qr-sub {
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
color: #1a1a1a;
text-align: center;
line-height: 1.4;
opacity: 0.65;
max-width: 190px;
}
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
QR CODE — SVG grid rendered in pure black/white
Pattern: encodes WIFI:S:PictureFrame-A3F7;T:nopass;;
This is a realistic-looking QR placeholder.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
</style>
</head>
<body>
<div class="device-bezel">
<div class="display">
<!-- STATUS BAR -->
<div class="status-bar">
<span class="status-bar-label">Setup Mode &mdash; Step 1 of 2</span>
<span class="status-bar-network">
Broadcasting: <span class="network-chip">PictureFrame-A3F7</span>
</span>
</div>
<!-- BODY -->
<div class="body">
<!-- LEFT: instructions -->
<div class="panel-left">
<div>
<div class="main-heading">Connect to<br><em>WiFi</em></div>
<ul class="step-list">
<li class="step-item">
<div class="step-num">1</div>
<div class="step-text">
Scan the QR code &rarr;<br>
Your phone joins <strong>PictureFrame-A3F7</strong>
</div>
</li>
<li class="step-item">
<div class="step-num">2</div>
<div class="step-text">
A WiFi setup page opens automatically in your browser
</div>
</li>
<li class="step-item">
<div class="step-num">3</div>
<div class="step-text">
Enter your <strong>home WiFi</strong> credentials and tap Connect
</div>
</li>
</ul>
</div>
<div>
<div class="left-divider"></div>
<div class="footnote">
Page didn't open? Navigate to<br>
<strong>192.168.4.1</strong> in any browser.
</div>
</div>
</div>
<!-- CENTER: orientation diagrams -->
<div class="panel-center">
<div class="orient-section-title">Frame orientation</div>
<!-- Landscape (active) -->
<div class="orient-block orient-active">
<div class="orient-label">Landscape</div>
<div class="orient-landscape-frame"></div>
<div class="orient-landscape-ribbon"></div>
<div class="active-badge">&#10003;</div>
</div>
<div class="orient-divider"></div>
<!-- Portrait -->
<div class="orient-block">
<div class="orient-label">Portrait</div>
<div class="orient-portrait-wrapper">
<div class="orient-portrait-ribbon"></div>
<div class="orient-portrait-frame"></div>
</div>
</div>
</div>
<!-- RIGHT: QR code -->
<div class="panel-right">
<div class="qr-instruction">Scan to connect</div>
<div class="qr-wrapper">
<!-- QR code rendered as SVG — encodes WIFI:S:PictureFrame-A3F7;T:nopass;; -->
<svg width="178" height="178" viewBox="0 0 41 41" xmlns="http://www.w3.org/2000/svg"
shape-rendering="crispEdges">
<rect width="41" height="41" fill="#f5f5f0"/>
<!-- TOP-LEFT FINDER PATTERN -->
<rect x="1" y="1" width="7" height="7" fill="#1a1a1a"/>
<rect x="2" y="2" width="5" height="5" fill="#f5f5f0"/>
<rect x="3" y="3" width="3" height="3" fill="#1a1a1a"/>
<!-- TOP-RIGHT FINDER PATTERN -->
<rect x="33" y="1" width="7" height="7" fill="#1a1a1a"/>
<rect x="34" y="2" width="5" height="5" fill="#f5f5f0"/>
<rect x="35" y="3" width="3" height="3" fill="#1a1a1a"/>
<!-- BOTTOM-LEFT FINDER PATTERN -->
<rect x="1" y="33" width="7" height="7" fill="#1a1a1a"/>
<rect x="2" y="34" width="5" height="5" fill="#f5f5f0"/>
<rect x="3" y="35" width="3" height="3" fill="#1a1a1a"/>
<!-- TIMING PATTERNS (horizontal and vertical) -->
<rect x="9" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="11" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="13" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="15" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="19" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="21" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="23" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="25" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="27" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="29" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="31" y="6" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="9" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="11" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="13" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="15" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="17" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="19" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="21" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="23" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="25" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="27" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="29" width="1" height="1" fill="#1a1a1a"/>
<rect x="6" y="31" width="1" height="1" fill="#1a1a1a"/>
<!-- DATA MODULES — WiFi QR payload simulation -->
<!-- Row 9 -->
<rect x="9" y="9" width="1" height="1" fill="#1a1a1a"/>
<rect x="11" y="9" width="2" height="1" fill="#1a1a1a"/>
<rect x="14" y="9" width="1" height="1" fill="#1a1a1a"/>
<rect x="16" y="9" width="3" height="1" fill="#1a1a1a"/>
<rect x="21" y="9" width="2" height="1" fill="#1a1a1a"/>
<rect x="25" y="9" width="1" height="1" fill="#1a1a1a"/>
<rect x="27" y="9" width="2" height="1" fill="#1a1a1a"/>
<rect x="31" y="9" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 10 -->
<rect x="9" y="10" width="2" height="1" fill="#1a1a1a"/>
<rect x="13" y="10" width="1" height="1" fill="#1a1a1a"/>
<rect x="15" y="10" width="2" height="1" fill="#1a1a1a"/>
<rect x="19" y="10" width="1" height="1" fill="#1a1a1a"/>
<rect x="22" y="10" width="3" height="1" fill="#1a1a1a"/>
<rect x="26" y="10" width="2" height="1" fill="#1a1a1a"/>
<rect x="30" y="10" width="2" height="1" fill="#1a1a1a"/>
<!-- Row 11 -->
<rect x="8" y="11" width="1" height="1" fill="#1a1a1a"/>
<rect x="10" y="11" width="3" height="1" fill="#1a1a1a"/>
<rect x="15" y="11" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="11" width="2" height="1" fill="#1a1a1a"/>
<rect x="21" y="11" width="1" height="1" fill="#1a1a1a"/>
<rect x="24" y="11" width="2" height="1" fill="#1a1a1a"/>
<rect x="28" y="11" width="3" height="1" fill="#1a1a1a"/>
<!-- Row 12 -->
<rect x="9" y="12" width="4" height="1" fill="#1a1a1a"/>
<rect x="14" y="12" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="12" width="3" height="1" fill="#1a1a1a"/>
<rect x="22" y="12" width="2" height="1" fill="#1a1a1a"/>
<rect x="26" y="12" width="1" height="1" fill="#1a1a1a"/>
<rect x="29" y="12" width="2" height="1" fill="#1a1a1a"/>
<rect x="33" y="12" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 13 -->
<rect x="8" y="13" width="2" height="1" fill="#1a1a1a"/>
<rect x="12" y="13" width="1" height="1" fill="#1a1a1a"/>
<rect x="14" y="13" width="2" height="1" fill="#1a1a1a"/>
<rect x="18" y="13" width="4" height="1" fill="#1a1a1a"/>
<rect x="24" y="13" width="3" height="1" fill="#1a1a1a"/>
<rect x="29" y="13" width="1" height="1" fill="#1a1a1a"/>
<rect x="31" y="13" width="2" height="1" fill="#1a1a1a"/>
<!-- Row 14 -->
<rect x="9" y="14" width="1" height="1" fill="#1a1a1a"/>
<rect x="11" y="14" width="3" height="1" fill="#1a1a1a"/>
<rect x="16" y="14" width="2" height="1" fill="#1a1a1a"/>
<rect x="20" y="14" width="1" height="1" fill="#1a1a1a"/>
<rect x="23" y="14" width="2" height="1" fill="#1a1a1a"/>
<rect x="27" y="14" width="3" height="1" fill="#1a1a1a"/>
<rect x="32" y="14" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 15 -->
<rect x="8" y="15" width="3" height="1" fill="#1a1a1a"/>
<rect x="13" y="15" width="2" height="1" fill="#1a1a1a"/>
<rect x="17" y="15" width="1" height="1" fill="#1a1a1a"/>
<rect x="19" y="15" width="3" height="1" fill="#1a1a1a"/>
<rect x="24" y="15" width="1" height="1" fill="#1a1a1a"/>
<rect x="27" y="15" width="2" height="1" fill="#1a1a1a"/>
<rect x="30" y="15" width="3" height="1" fill="#1a1a1a"/>
<!-- Row 16 -->
<rect x="9" y="16" width="2" height="1" fill="#1a1a1a"/>
<rect x="13" y="16" width="1" height="1" fill="#1a1a1a"/>
<rect x="16" y="16" width="3" height="1" fill="#1a1a1a"/>
<rect x="21" y="16" width="2" height="1" fill="#1a1a1a"/>
<rect x="25" y="16" width="3" height="1" fill="#1a1a1a"/>
<rect x="30" y="16" width="2" height="1" fill="#1a1a1a"/>
<rect x="34" y="16" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 17 -->
<rect x="8" y="17" width="1" height="1" fill="#1a1a1a"/>
<rect x="10" y="17" width="4" height="1" fill="#1a1a1a"/>
<rect x="15" y="17" width="2" height="1" fill="#1a1a1a"/>
<rect x="19" y="17" width="1" height="1" fill="#1a1a1a"/>
<rect x="22" y="17" width="3" height="1" fill="#1a1a1a"/>
<rect x="27" y="17" width="1" height="1" fill="#1a1a1a"/>
<rect x="29" y="17" width="4" height="1" fill="#1a1a1a"/>
<!-- Row 18 -->
<rect x="9" y="18" width="3" height="1" fill="#1a1a1a"/>
<rect x="14" y="18" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="18" width="2" height="1" fill="#1a1a1a"/>
<rect x="21" y="18" width="1" height="1" fill="#1a1a1a"/>
<rect x="24" y="18" width="2" height="1" fill="#1a1a1a"/>
<rect x="28" y="18" width="2" height="1" fill="#1a1a1a"/>
<rect x="32" y="18" width="3" height="1" fill="#1a1a1a"/>
<!-- Row 19 -->
<rect x="8" y="19" width="2" height="1" fill="#1a1a1a"/>
<rect x="12" y="19" width="3" height="1" fill="#1a1a1a"/>
<rect x="16" y="19" width="1" height="1" fill="#1a1a1a"/>
<rect x="18" y="19" width="2" height="1" fill="#1a1a1a"/>
<rect x="22" y="19" width="4" height="1" fill="#1a1a1a"/>
<rect x="28" y="19" width="1" height="1" fill="#1a1a1a"/>
<rect x="30" y="19" width="3" height="1" fill="#1a1a1a"/>
<!-- Row 20 (center) -->
<rect x="9" y="20" width="1" height="1" fill="#1a1a1a"/>
<rect x="11" y="20" width="2" height="1" fill="#1a1a1a"/>
<rect x="15" y="20" width="3" height="1" fill="#1a1a1a"/>
<rect x="20" y="20" width="1" height="1" fill="#1a1a1a"/>
<rect x="23" y="20" width="2" height="1" fill="#1a1a1a"/>
<rect x="27" y="20" width="3" height="1" fill="#1a1a1a"/>
<rect x="32" y="20" width="2" height="1" fill="#1a1a1a"/>
<!-- Row 21 -->
<rect x="8" y="21" width="4" height="1" fill="#1a1a1a"/>
<rect x="14" y="21" width="2" height="1" fill="#1a1a1a"/>
<rect x="18" y="21" width="3" height="1" fill="#1a1a1a"/>
<rect x="23" y="21" width="1" height="1" fill="#1a1a1a"/>
<rect x="25" y="21" width="2" height="1" fill="#1a1a1a"/>
<rect x="29" y="21" width="1" height="1" fill="#1a1a1a"/>
<rect x="31" y="21" width="3" height="1" fill="#1a1a1a"/>
<!-- Row 22 -->
<rect x="9" y="22" width="2" height="1" fill="#1a1a1a"/>
<rect x="13" y="22" width="1" height="1" fill="#1a1a1a"/>
<rect x="16" y="22" width="2" height="1" fill="#1a1a1a"/>
<rect x="20" y="22" width="2" height="1" fill="#1a1a1a"/>
<rect x="24" y="22" width="3" height="1" fill="#1a1a1a"/>
<rect x="29" y="22" width="2" height="1" fill="#1a1a1a"/>
<rect x="33" y="22" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 23 -->
<rect x="8" y="23" width="1" height="1" fill="#1a1a1a"/>
<rect x="10" y="23" width="3" height="1" fill="#1a1a1a"/>
<rect x="15" y="23" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="23" width="3" height="1" fill="#1a1a1a"/>
<rect x="22" y="23" width="2" height="1" fill="#1a1a1a"/>
<rect x="26" y="23" width="1" height="1" fill="#1a1a1a"/>
<rect x="28" y="23" width="4" height="1" fill="#1a1a1a"/>
<!-- Row 24 -->
<rect x="9" y="24" width="4" height="1" fill="#1a1a1a"/>
<rect x="14" y="24" width="2" height="1" fill="#1a1a1a"/>
<rect x="18" y="24" width="1" height="1" fill="#1a1a1a"/>
<rect x="21" y="24" width="3" height="1" fill="#1a1a1a"/>
<rect x="26" y="24" width="2" height="1" fill="#1a1a1a"/>
<rect x="30" y="24" width="1" height="1" fill="#1a1a1a"/>
<rect x="32" y="24" width="2" height="1" fill="#1a1a1a"/>
<!-- Row 25 -->
<rect x="8" y="25" width="2" height="1" fill="#1a1a1a"/>
<rect x="12" y="25" width="1" height="1" fill="#1a1a1a"/>
<rect x="14" y="25" width="3" height="1" fill="#1a1a1a"/>
<rect x="19" y="25" width="2" height="1" fill="#1a1a1a"/>
<rect x="23" y="25" width="1" height="1" fill="#1a1a1a"/>
<rect x="26" y="25" width="3" height="1" fill="#1a1a1a"/>
<rect x="31" y="25" width="3" height="1" fill="#1a1a1a"/>
<!-- Row 26 -->
<rect x="9" y="26" width="1" height="1" fill="#1a1a1a"/>
<rect x="11" y="26" width="2" height="1" fill="#1a1a1a"/>
<rect x="15" y="26" width="2" height="1" fill="#1a1a1a"/>
<rect x="19" y="26" width="1" height="1" fill="#1a1a1a"/>
<rect x="22" y="26" width="3" height="1" fill="#1a1a1a"/>
<rect x="27" y="26" width="2" height="1" fill="#1a1a1a"/>
<rect x="31" y="26" width="1" height="1" fill="#1a1a1a"/>
<rect x="33" y="26" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 27 -->
<rect x="8" y="27" width="3" height="1" fill="#1a1a1a"/>
<rect x="13" y="27" width="1" height="1" fill="#1a1a1a"/>
<rect x="16" y="27" width="3" height="1" fill="#1a1a1a"/>
<rect x="21" y="27" width="2" height="1" fill="#1a1a1a"/>
<rect x="25" y="27" width="1" height="1" fill="#1a1a1a"/>
<rect x="28" y="27" width="3" height="1" fill="#1a1a1a"/>
<rect x="33" y="27" width="2" height="1" fill="#1a1a1a"/>
<!-- Row 28 -->
<rect x="9" y="28" width="2" height="1" fill="#1a1a1a"/>
<rect x="13" y="28" width="3" height="1" fill="#1a1a1a"/>
<rect x="18" y="28" width="1" height="1" fill="#1a1a1a"/>
<rect x="21" y="28" width="1" height="1" fill="#1a1a1a"/>
<rect x="24" y="28" width="2" height="1" fill="#1a1a1a"/>
<rect x="28" y="28" width="1" height="1" fill="#1a1a1a"/>
<rect x="30" y="28" width="4" height="1" fill="#1a1a1a"/>
<!-- Row 29 -->
<rect x="8" y="29" width="1" height="1" fill="#1a1a1a"/>
<rect x="10" y="29" width="4" height="1" fill="#1a1a1a"/>
<rect x="16" y="29" width="2" height="1" fill="#1a1a1a"/>
<rect x="20" y="29" width="3" height="1" fill="#1a1a1a"/>
<rect x="25" y="29" width="2" height="1" fill="#1a1a1a"/>
<rect x="29" y="29" width="2" height="1" fill="#1a1a1a"/>
<rect x="33" y="29" width="1" height="1" fill="#1a1a1a"/>
<!-- Row 30 -->
<rect x="9" y="30" width="3" height="1" fill="#1a1a1a"/>
<rect x="14" y="30" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="30" width="1" height="1" fill="#1a1a1a"/>
<rect x="20" y="30" width="2" height="1" fill="#1a1a1a"/>
<rect x="24" y="30" width="3" height="1" fill="#1a1a1a"/>
<rect x="29" y="30" width="1" height="1" fill="#1a1a1a"/>
<rect x="32" y="30" width="2" height="1" fill="#1a1a1a"/>
<!-- Row 31 -->
<rect x="8" y="31" width="2" height="1" fill="#1a1a1a"/>
<rect x="12" y="31" width="2" height="1" fill="#1a1a1a"/>
<rect x="16" y="31" width="3" height="1" fill="#1a1a1a"/>
<rect x="21" y="31" width="1" height="1" fill="#1a1a1a"/>
<rect x="23" y="31" width="2" height="1" fill="#1a1a1a"/>
<rect x="27" y="31" width="4" height="1" fill="#1a1a1a"/>
<rect x="33" y="31" width="1" height="1" fill="#1a1a1a"/>
<!-- Alignment pattern bottom-right (5×5) -->
<rect x="28" y="28" width="5" height="5" fill="#1a1a1a"/>
<rect x="29" y="29" width="3" height="3" fill="#f5f5f0"/>
<rect x="30" y="30" width="1" height="1" fill="#1a1a1a"/>
<!-- Format info strips -->
<rect x="8" y="8" width="1" height="1" fill="#1a1a1a"/>
<!-- Extra data density rows 3339 (below bottom-left finder) -->
<rect x="9" y="34" width="2" height="1" fill="#1a1a1a"/>
<rect x="13" y="34" width="1" height="1" fill="#1a1a1a"/>
<rect x="15" y="34" width="3" height="1" fill="#1a1a1a"/>
<rect x="20" y="34" width="2" height="1" fill="#1a1a1a"/>
<rect x="24" y="34" width="1" height="1" fill="#1a1a1a"/>
<rect x="27" y="34" width="2" height="1" fill="#1a1a1a"/>
<rect x="8" y="35" width="3" height="1" fill="#1a1a1a"/>
<rect x="13" y="35" width="2" height="1" fill="#1a1a1a"/>
<rect x="17" y="35" width="1" height="1" fill="#1a1a1a"/>
<rect x="19" y="35" width="3" height="1" fill="#1a1a1a"/>
<rect x="24" y="35" width="2" height="1" fill="#1a1a1a"/>
<rect x="28" y="35" width="1" height="1" fill="#1a1a1a"/>
<rect x="9" y="36" width="1" height="1" fill="#1a1a1a"/>
<rect x="11" y="36" width="2" height="1" fill="#1a1a1a"/>
<rect x="14" y="36" width="1" height="1" fill="#1a1a1a"/>
<rect x="17" y="36" width="2" height="1" fill="#1a1a1a"/>
<rect x="21" y="36" width="1" height="1" fill="#1a1a1a"/>
<rect x="23" y="36" width="3" height="1" fill="#1a1a1a"/>
<rect x="27" y="36" width="2" height="1" fill="#1a1a1a"/>
<rect x="8" y="37" width="4" height="1" fill="#1a1a1a"/>
<rect x="14" y="37" width="2" height="1" fill="#1a1a1a"/>
<rect x="18" y="37" width="3" height="1" fill="#1a1a1a"/>
<rect x="23" y="37" width="1" height="1" fill="#1a1a1a"/>
<rect x="25" y="37" width="2" height="1" fill="#1a1a1a"/>
<rect x="29" y="37" width="1" height="1" fill="#1a1a1a"/>
<rect x="9" y="38" width="2" height="1" fill="#1a1a1a"/>
<rect x="13" y="38" width="3" height="1" fill="#1a1a1a"/>
<rect x="18" y="38" width="1" height="1" fill="#1a1a1a"/>
<rect x="21" y="38" width="2" height="1" fill="#1a1a1a"/>
<rect x="25" y="38" width="3" height="1" fill="#1a1a1a"/>
<rect x="30" y="38" width="1" height="1" fill="#1a1a1a"/>
<rect x="8" y="39" width="1" height="1" fill="#1a1a1a"/>
<rect x="10" y="39" width="3" height="1" fill="#1a1a1a"/>
<rect x="15" y="39" width="2" height="1" fill="#1a1a1a"/>
<rect x="19" y="39" width="1" height="1" fill="#1a1a1a"/>
<rect x="22" y="39" width="2" height="1" fill="#1a1a1a"/>
<rect x="26" y="39" width="1" height="1" fill="#1a1a1a"/>
<rect x="28" y="39" width="3" height="1" fill="#1a1a1a"/>
</svg>
</div>
<div class="qr-sub">
Encodes: <strong>WIFI:S:PictureFrame-A3F7;T:nopass;;</strong>
</div>
</div>
</div><!-- /.body -->
</div><!-- /.display -->
</div><!-- /.device-bezel -->
<!--
DESIGNER NOTES
──────────────────────────────────────────────────────────────────
Screen: AP Provisioning — Step 1 of 2
State: Frame has no WiFi credentials. Broadcasting open AP.
Accent: YELLOW — signals "action required," unconnected state.
Layout rationale:
• Yellow status bar carries SSID at a glance — one horizontal scan
tells you what network to join without reading the instructions.
• Instructions live LEFT, not centered, to make room for the large QR
without any elements competing at the same scale.
• Orientation diagrams are narrow + centered — they confirm physical
placement, not the primary action. They don't compete.
• QR panel is RIGHT and large — the single action. Yellow bracket
border echoes the accent and frames the eye's destination.
• Step numbers in black/yellow inverse boxes are readable at a
distance; the QR is readable up close. Both jobs done together.
• "Landscape" is marked active (check mark + yellow ribbon) because
the frame is currently held landscape. Portrait shown as the alt.
• Footnote handles the "captive portal didn't open" edge case.
Firmware MUST handle this — the screen already does.
Firmware implementation notes (for when you write the C):
• The SSID suffix is the last 4 hex chars of the MAC.
• The QR encodes the WIFI: string for auto-join on iOS/Android.
• The 192.168.4.1 fallback is the ESP32 SoftAP default gateway.
──────────────────────────────────────────────────────────────────
-->
</body>
</html>