feat: badge print — two-line name toggle + leading-none tightening + calibration SVG

- Add name_two_lines toggle (default: true) — uses CSS horizontal padding scaled by
  character count to coax short names (e.g. "Scott Idem") into a natural two-line wrap
  without a hard <br>; three tiers: ≤12 chars (18%), ≤20 (8%), ≤28 (2%), >28 no pad
- Inner <div> (block element) used inside Element_fit_text for class: directives —
  Svelte scoped CSS requires static class names in the template; dynamic strings and
  class: on component elements both fail to match scoped CSS rules
- Add leading-none to all four Element_fit_text fields (name, title, affiliations,
  location) — line-height must be set at the wrapper div level where fit_text measures
  scrollHeight, otherwise the binary-search scaler returns inflated sizes
- name_two_lines state persisted to localStorage (ae_badge_print_tweaks key) alongside
  existing print_offset, hide_chrome, and banner_full_width tweaks
- Rewrite badge_header_calibration.svg as a precise SVG ruler with labeled tick marks
  (major at 1in intervals, minor at 0.25in) for accurate physical print calibration
- Gate debug outline CSS on html.debug_outlines class (set by controls panel) so
  outlines never appear in normal print mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-19 17:55:49 -04:00
parent 621a637b85
commit fdd8691e2e
5 changed files with 272 additions and 32 deletions

View File

@@ -43,6 +43,13 @@
* Set false via the controls panel to see the image at its natural size (dev/testing).
*/
banner_full_width?: boolean;
/**
* When true (default), the person's name is split at the last space and rendered
* on two lines. Gives short names like "Scott Idem" the same two-line visual weight
* as long names. Set false for names that genuinely read better on one line.
* Toggle lives in the controls panel; persisted in localStorage per workstation.
*/
name_two_lines?: boolean;
log_lvl?: number;
}
@@ -58,6 +65,7 @@
font_size_location,
preview_overrides = null,
banner_full_width = true,
name_two_lines = true,
}: Props = $props();
// Badge layout CSS — compiled in, hot-reloads in dev.
@@ -102,6 +110,19 @@
let display_name = $derived(
eff_badge?.full_name_override ?? eff_badge?.full_name ?? ''
);
// Two-line name padding: when name_two_lines is on, apply horizontal padding scaled
// to name length so CSS word-wrap does the break naturally — no hard <br> needed.
// Short names (≤12 chars) get heavy padding to force a break at the space.
// Progressively less padding as names get longer; very long names wrap on their own.
// Breakpoints are character-count heuristics — adjust if specific names look off.
// name_pad_* — three tiers of horizontal padding to coax short names into two lines.
// Svelte scopes component style rules via compile-time class name hashing; dynamic class
// strings are invisible to the compiler so the hash never matches. class:name={cond} is the
// correct pattern — the literal class names appear in the template for the compiler to find.
let name_pad_short = $derived(name_two_lines && display_name.trim().length <= 12);
let name_pad_mid = $derived(name_two_lines && display_name.trim().length > 12 && display_name.trim().length <= 20);
let name_pad_long = $derived(name_two_lines && display_name.trim().length > 20 && display_name.trim().length <= 28);
let display_title = $derived(
eff_badge?.professional_title_override ?? eff_badge?.professional_title ?? ''
);
@@ -258,20 +279,20 @@
const [bg, fg] = palettes[effective_badge_type_code?.toLowerCase() ?? ''] ?? ['#f1f5f9', '#64748b'];
// Each entry: [svg string, tile size]. Swirls use a larger tile for smoother repeats.
const patterns: [string, string][] = [
// 1: diagonal stripes
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='0' y1='24' x2='24' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='-12' y1='24' x2='12' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='12' y1='24' x2='36' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
// 2: polka dots
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><circle cx='12' cy='12' r='5' fill='${fg}' fill-opacity='0.35'/></svg>`, '24px 24px'],
// 3: diamonds
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><polygon points='12,3 21,12 12,21 3,12' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
// 4: grid
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='12' y1='0' x2='12' y2='24' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/><line x1='0' y1='12' x2='24' y2='12' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/></svg>`, '24px 24px'],
// 5: swirls — two crossing S-curves (yin-yang tiling) + echo curves for depth.
[`<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64'><rect width='64' height='64' fill='${bg}'/><path d='M0,0 C0,32 64,32 64,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M64,0 C64,32 0,32 0,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M0,16 C16,32 48,32 64,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><path d='M64,16 C48,32 16,32 0,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><circle cx='32' cy='32' r='4' fill='${fg}' fill-opacity='0.18'/></svg>`, '64px 64px'],
// 6: Inch Calibration Grid (1 inch squares, 0.25 inch subdivisions).
[`<svg xmlns='http://www.w3.org/2000/svg' width='96' height='96'><rect width='96' height='96' fill='white' fill-opacity='0.9'/><path d='M24 0 v96 M48 0 v96 M72 0 v96 M0 24 h96 M0 48 h96 M0 72 h96' stroke='${fg}' stroke-width='0.5' stroke-opacity='0.2'/><rect width='96' height='96' fill='none' stroke='${fg}' stroke-width='1' stroke-opacity='0.5'/><text x='2' y='10' font-family='monospace' font-size='8' fill='${fg}' opacity='0.5'>1in</text></svg>`, '96px 96px'],
// 7: Metric Calibration Grid (10mm squares, 1mm subdivisions).
// 1: Metric Calibration Grid (10mm squares, 1mm subdivisions).
[`<svg xmlns='http://www.w3.org/2000/svg' width='37.795' height='37.795'><rect width='37.795' height='37.795' fill='white' fill-opacity='0.9'/><path d='M3.78 0 v37.8 M7.56 0 v37.8 M11.34 0 v37.8 M15.12 0 v37.8 M18.9 0 v37.8 M22.68 0 v37.8 M26.46 0 v37.8 M30.24 0 v37.8 M34.02 0 v37.8 M0 3.78 h37.8 M0 7.56 h37.8 M0 11.34 h37.8 M0 15.12 h37.8 M0 18.9 h37.8 M0 22.68 h37.8 M0 26.46 h37.8 M0 30.24 h37.8 M0 34.02 h37.8' stroke='${fg}' stroke-width='0.25' stroke-opacity='0.2'/><rect width='37.795' height='37.795' fill='none' stroke='${fg}' stroke-width='0.8' stroke-opacity='0.5'/><text x='2' y='8' font-family='monospace' font-size='6' fill='${fg}' opacity='0.5'>1cm</text></svg>`, '37.795px 37.795px'],
// 2: Inch Calibration Grid (1 inch squares, 0.25 inch subdivisions).
[`<svg xmlns='http://www.w3.org/2000/svg' width='96' height='96'><rect width='96' height='96' fill='white' fill-opacity='0.9'/><path d='M24 0 v96 M48 0 v96 M72 0 v96 M0 24 h96 M0 48 h96 M0 72 h96' stroke='${fg}' stroke-width='0.5' stroke-opacity='0.2'/><rect width='96' height='96' fill='none' stroke='${fg}' stroke-width='1' stroke-opacity='0.5'/><text x='2' y='10' font-family='monospace' font-size='8' fill='${fg}' opacity='0.5'>1in</text></svg>`, '96px 96px'],
// 3: swirls — two crossing S-curves (yin-yang tiling) + echo curves for depth.
[`<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64'><rect width='64' height='64' fill='${bg}'/><path d='M0,0 C0,32 64,32 64,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M64,0 C64,32 0,32 0,64' fill='none' stroke='${fg}' stroke-width='2.5' stroke-opacity='0.45' stroke-linecap='round'/><path d='M0,16 C16,32 48,32 64,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><path d='M64,16 C48,32 16,32 0,48' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.22' stroke-linecap='round'/><circle cx='32' cy='32' r='4' fill='${fg}' fill-opacity='0.18'/></svg>`, '64px 64px'],
// 4: diagonal stripes
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='0' y1='24' x2='24' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='-12' y1='24' x2='12' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/><line x1='12' y1='24' x2='36' y2='0' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
// 5: polka dots
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><circle cx='12' cy='12' r='5' fill='${fg}' fill-opacity='0.35'/></svg>`, '24px 24px'],
// 6: diamonds
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><polygon points='12,3 21,12 12,21 3,12' fill='none' stroke='${fg}' stroke-width='1.5' stroke-opacity='0.5'/></svg>`, '24px 24px'],
// 7: grid
[`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><rect width='24' height='24' fill='${bg}'/><line x1='12' y1='0' x2='12' y2='24' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/><line x1='0' y1='12' x2='24' y2='12' stroke='${fg}' stroke-width='1' stroke-opacity='0.4'/></svg>`, '24px 24px'],
];
const [svg, size] = patterns[(demo_bg_state - 1) % DEMO_BG_PATTERNS];
return `background-image: url("data:image/svg+xml,${encodeURIComponent(svg)}"); background-repeat: repeat; background-size: ${size};`;
@@ -336,7 +357,7 @@
onclick={cycle_demo_bg}
title="Cycle demo background pattern edit mode only"
>
🎨 {demo_bg_state > 0 ? ['stripes','dots','diamonds','grid','swirls','in-grid','mm-grid'][demo_bg_state - 1] : 'demo bg'}
🎨 {demo_bg_state > 0 ? ['mm-grid','in-grid','swirls','stripes','dots','diamonds','grid'][demo_bg_state - 1] : 'demo bg'}
</button>
{/if}
</div>
@@ -348,7 +369,6 @@
flex flex-row flex-wrap gap-4
items-stretch justify-center
p-2 mx-auto
outline-2 outline-dashed outline-blue-500
min-h-[6.0in]
max-w-[8.5in] overflow-visible
"
@@ -367,7 +387,7 @@
p-0 m-0 overflow-visible
text-center
relative
outline-4 outline-red-500/50 hover:outline-red-700/75
hover:outline-2 hover:outline-dashed hover:outline-red-500/75
group
"
style={demo_bg_style}
@@ -455,15 +475,20 @@
max={80}
manual_size={font_size_name ?? null}
height={fit_heights.name}
class="full_name_override_all hover:bg-pink-100/50"
class="full_name_override_all hover:bg-pink-100/50 leading-none"
>
<span class="full_name_override">
<div
class="full_name_override"
class:name_pad_short={name_pad_short}
class:name_pad_mid={name_pad_mid}
class:name_pad_long={name_pad_long}
>
{#if display_name}
{@html display_name.trim()}
{display_name.trim()}
{:else}
-- no name --
{/if}
</span>
</div>
</Element_fit_text>
{#if display_title}
@@ -476,7 +501,7 @@
max={38}
manual_size={font_size_title ?? null}
height={fit_heights.title}
class="professional_title italic hover:bg-pink-100/50"
class="professional_title italic hover:bg-pink-100/50 leading-none"
>
{@html display_title}
</Element_fit_text>
@@ -508,7 +533,7 @@
max={40}
manual_size={font_size_affiliations ?? null}
height={fit_heights.affiliations}
class="affiliations hover:bg-pink-100/50"
class="affiliations hover:bg-pink-100/50 leading-none"
>
{@html display_affiliations}
</Element_fit_text>
@@ -524,7 +549,7 @@
max={34}
manual_size={font_size_location ?? null}
height={fit_heights.location}
class="location hover:bg-pink-100/50"
class="location hover:bg-pink-100/50 leading-none"
>
<span class="city state_province country"
>{@html display_location}</span
@@ -619,7 +644,7 @@
p-0 m-0 overflow-visible
text-left text-sm
relative
outline-4 outline-green-500/50 hover:outline-green-700/75
hover:outline-2 hover:outline-dashed hover:outline-green-500/75
group
"
>
@@ -974,6 +999,7 @@
.badge_front,
.badge_back {
outline: none !important;
box-shadow: none !important;
}
}
@@ -981,8 +1007,37 @@
.badge_back {
background-color: white;
color: #1a1a1a;
/*
* Standard badge/card corner radius: 3.18mm = 1/8 inch = ~12px at 96dpi.
* Matches the physical die-cut corners of the printed card stock.
*/
border-radius: 12px;
/*
* Layered card shadow: gives each badge face a strong physical-card feel on screen.
* Three layers: crisp base edge → medium spread → wide ambient — creates
* convincing depth without looking like a UI box-shadow.
* Suppressed on print — no decoration on the physical output.
*/
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.20),
0 6px 20px rgba(0, 0, 0, 0.14),
0 16px 48px rgba(0, 0, 0, 0.08);
}
/*
* Two-line name padding: applied to the Element_fit_text wrapper div (not the inner span).
* Squeezes the available text width so CSS word-wrap breaks the name at its natural space
* — no hard <br> needed. Padding on the block div also ensures text-align:center works
* correctly for all wrapped lines (inline-element padding breaks centering on each line).
* Short names (≤12 chars, e.g. "Scott Idem") need heavy padding to force the break.
* Progressively less padding as names get longer; very long names wrap on their own.
* Percentages are relative to the Element_fit_text div width, so the effect scales
* correctly regardless of badge template or font size.
*/
.name_pad_short { padding-left: 18%; padding-right: 18%; }
.name_pad_mid { padding-left: 8%; padding-right: 8%; }
.name_pad_long { padding-left: 2%; padding-right: 2%; }
/*
* Header image: center horizontally within the badge.
* <img> defaults to display:inline, which left-aligns any image narrower than

View File

@@ -60,6 +60,8 @@
* Stored in localStorage.
*/
banner_full_width?: boolean;
/** When true (default), name uses padding to prefer two-line layout. Stored in localStorage. */
name_two_lines?: boolean;
log_lvl?: number;
}
@@ -77,6 +79,7 @@
print_offset_y = $bindable(0),
hide_chrome = $bindable(false),
banner_full_width = $bindable(true),
name_two_lines = $bindable(true),
log_lvl = 0
}: Props = $props();
@@ -929,6 +932,18 @@
</button>
</div>
<!-- Name layout: two-line (preferred) vs single-line -->
<div class="flex items-center gap-2 px-2 py-1.5">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
class="checkbox"
bind:checked={name_two_lines}
/>
<span class="text-xs">Name: prefer two lines</span>
</label>
</div>
<!-- Banner width toggle: 100% (production) vs natural size (dev/calibration) -->
<div class="flex items-center gap-2 px-2 py-1.5">
<label class="flex items-center gap-2 cursor-pointer select-none">

View File

@@ -122,6 +122,11 @@
// Banner width: true = 100% (production), false = natural pixel size (dev/calibration).
let banner_full_width: boolean = $state(_saved_tweaks?.banner_full_width ?? true);
// Name layout: true = prefer two lines (first / last split), false = one line.
// Default true — most badge templates look best with the name split across two lines.
// Stored in localStorage so the preference persists per workstation across reloads.
let name_two_lines: boolean = $state(_saved_tweaks?.name_two_lines ?? true);
// hide_chrome: hides the page header and sys bar for a clean badge-preview workspace.
// Toggled via the [H] keyboard shortcut or the button in the controls panel staff section.
let hide_chrome: boolean = $state(_saved_tweaks?.hide_chrome ?? false);
@@ -147,7 +152,7 @@
// Persist all tweaks to localStorage
$effect(() => {
if (browser) localStorage.setItem(_PRINT_TWEAKS_KEY, JSON.stringify({ x: print_offset_x, y: print_offset_y, hide_chrome, banner_full_width }));
if (browser) localStorage.setItem(_PRINT_TWEAKS_KEY, JSON.stringify({ x: print_offset_x, y: print_offset_y, hide_chrome, banner_full_width, name_two_lines }));
});
// Inject print offset as @media print CSS. Uses translate() so the centering
@@ -374,6 +379,7 @@
font_size_location={font_size_location}
{preview_overrides}
{banner_full_width}
{name_two_lines}
/>
</div>
@@ -403,6 +409,7 @@
bind:print_offset_y
bind:hide_chrome
bind:banner_full_width
bind:name_two_lines
/>
</div>

View File

@@ -107,9 +107,10 @@
outline: 3px dashed purple !important;
outline-offset: -12px !important;
}
/* Cyan = the actual badge — should be dead-center on page */
/* Red = the actual badge — should be dead-center on page.
Thick + high-contrast so misalignment vs. physical card stock is obvious. */
html.debug_outlines .event_badge_wrapper {
outline: 3px solid cyan !important;
outline-offset: 2px !important;
outline: 6px solid red !important;
outline-offset: 3px !important;
}
}

View File

@@ -1,6 +1,168 @@
<svg width="332" height="72" viewBox="0 0 332 72" xmlns="http://www.w3.org/2000/svg">
<rect width="332" height="72" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1" />
<rect x="53" y="24" width="48" height="12" rx="4" fill="#ef4444" fill-opacity="0.4" stroke="#b91c1c" />
<rect x="231" y="24" width="48" height="12" rx="4" fill="#ef4444" fill-opacity="0.4" stroke="#b91c1c" />
<text x="166" y="42" font-family="monospace" font-size="10" fill="#ef4444" text-anchor="middle">CLIP SLOT DANGER ZONE</text>
<!--
Badge Header Calibration Strip — 88mm × 19mm (badge width × header slot height)
Scale: 332 SVG units = 88mm physical → 1mm = 3.7727px
332 SVG units = 3.464in physical → 1in = 95.85px
Use with banner_full_width=ON and debug print mode to verify horizontal alignment.
Top ruler: millimeters | Bottom ruler: inches
Red zones: lanyard clip slot danger areas
-->
<!-- Background -->
<rect width="332" height="72" fill="#f8fafc"/>
<!-- ════════════════════════════════════════════════════ -->
<!-- TOP RULER — millimeters (1mm = 3.7727px) -->
<!-- ════════════════════════════════════════════════════ -->
<!-- Faint separator between ruler and middle zone -->
<line x1="0" y1="17" x2="332" y2="17" stroke="#e2e8f0" stroke-width="0.5"/>
<!-- Unit label -->
<text x="328" y="8" font-family="sans-serif" font-size="5.5" fill="#94a3b8" font-style="italic" text-anchor="end">mm</text>
<!-- 1mm minor ticks (height=3) — all positions not on 5mm multiples -->
<line x1="3.77" y1="0" x2="3.77" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="7.55" y1="0" x2="7.55" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="11.32" y1="0" x2="11.32" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="15.09" y1="0" x2="15.09" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="22.64" y1="0" x2="22.64" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="26.41" y1="0" x2="26.41" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="30.18" y1="0" x2="30.18" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="33.95" y1="0" x2="33.95" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="41.50" y1="0" x2="41.50" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="45.27" y1="0" x2="45.27" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="49.05" y1="0" x2="49.05" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="52.82" y1="0" x2="52.82" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="60.36" y1="0" x2="60.36" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="64.14" y1="0" x2="64.14" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="67.91" y1="0" x2="67.91" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="71.68" y1="0" x2="71.68" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="79.23" y1="0" x2="79.23" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="83.00" y1="0" x2="83.00" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="86.77" y1="0" x2="86.77" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="90.55" y1="0" x2="90.55" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="98.09" y1="0" x2="98.09" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="101.86" y1="0" x2="101.86" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="105.64" y1="0" x2="105.64" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="109.41" y1="0" x2="109.41" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="116.95" y1="0" x2="116.95" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="120.73" y1="0" x2="120.73" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="124.50" y1="0" x2="124.50" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="128.27" y1="0" x2="128.27" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="135.82" y1="0" x2="135.82" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="139.59" y1="0" x2="139.59" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="143.36" y1="0" x2="143.36" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="147.14" y1="0" x2="147.14" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="154.68" y1="0" x2="154.68" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="158.45" y1="0" x2="158.45" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="162.23" y1="0" x2="162.23" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="166.00" y1="0" x2="166.00" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="173.55" y1="0" x2="173.55" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="177.32" y1="0" x2="177.32" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="181.09" y1="0" x2="181.09" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="184.86" y1="0" x2="184.86" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="192.41" y1="0" x2="192.41" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="196.18" y1="0" x2="196.18" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="199.95" y1="0" x2="199.95" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="203.73" y1="0" x2="203.73" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="211.27" y1="0" x2="211.27" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="215.05" y1="0" x2="215.05" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="218.82" y1="0" x2="218.82" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="222.59" y1="0" x2="222.59" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="230.14" y1="0" x2="230.14" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="233.91" y1="0" x2="233.91" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="237.68" y1="0" x2="237.68" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="241.45" y1="0" x2="241.45" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="249.00" y1="0" x2="249.00" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="252.77" y1="0" x2="252.77" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="256.55" y1="0" x2="256.55" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="260.32" y1="0" x2="260.32" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="267.86" y1="0" x2="267.86" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="271.64" y1="0" x2="271.64" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="275.41" y1="0" x2="275.41" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="279.18" y1="0" x2="279.18" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="286.73" y1="0" x2="286.73" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="290.50" y1="0" x2="290.50" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="294.27" y1="0" x2="294.27" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="298.05" y1="0" x2="298.05" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="305.59" y1="0" x2="305.59" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="309.36" y1="0" x2="309.36" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="313.14" y1="0" x2="313.14" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="316.91" y1="0" x2="316.91" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="324.45" y1="0" x2="324.45" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="328.23" y1="0" x2="328.23" y2="3" stroke="#94a3b8" stroke-width="0.75"/>
<!-- 5mm medium ticks (height=7) — at 5,15,25,...,85mm -->
<line x1="18.86" y1="0" x2="18.86" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="56.59" y1="0" x2="56.59" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="94.32" y1="0" x2="94.32" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="132.05" y1="0" x2="132.05" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="169.77" y1="0" x2="169.77" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="207.50" y1="0" x2="207.50" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="245.23" y1="0" x2="245.23" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="282.95" y1="0" x2="282.95" y2="7" stroke="#64748b" stroke-width="1"/>
<line x1="320.68" y1="0" x2="320.68" y2="7" stroke="#64748b" stroke-width="1"/>
<!-- 10mm major ticks (height=13) with labels — at 10,20,...,80mm -->
<line x1="37.73" y1="0" x2="37.73" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="37.73" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">10</text>
<line x1="75.45" y1="0" x2="75.45" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="75.45" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">20</text>
<line x1="113.18" y1="0" x2="113.18" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="113.18" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">30</text>
<line x1="150.91" y1="0" x2="150.91" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="150.91" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">40</text>
<line x1="188.64" y1="0" x2="188.64" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="188.64" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">50</text>
<line x1="226.36" y1="0" x2="226.36" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="226.36" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">60</text>
<line x1="264.09" y1="0" x2="264.09" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="264.09" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">70</text>
<line x1="301.82" y1="0" x2="301.82" y2="13" stroke="#334155" stroke-width="1.25"/>
<text x="301.82" y="12" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">80</text>
<!-- ════════════════════════════════════════════════════ -->
<!-- BOTTOM RULER — inches (1in = 95.85px) -->
<!-- ════════════════════════════════════════════════════ -->
<!-- Faint separator -->
<line x1="0" y1="55" x2="332" y2="55" stroke="#e2e8f0" stroke-width="0.5"/>
<!-- Unit label -->
<text x="328" y="65" font-family="sans-serif" font-size="5.5" fill="#94a3b8" font-style="italic" text-anchor="end">in</text>
<!-- 0.25in ticks (height=5) — at 0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25 in -->
<line x1="23.96" y1="72" x2="23.96" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="71.89" y1="72" x2="71.89" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="119.82" y1="72" x2="119.82" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="167.74" y1="72" x2="167.74" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="215.67" y1="72" x2="215.67" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="263.60" y1="72" x2="263.60" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<line x1="311.53" y1="72" x2="311.53" y2="67" stroke="#94a3b8" stroke-width="0.75"/>
<!-- 0.5in ticks (height=9) — at 0.5, 1.5, 2.5 in -->
<line x1="47.93" y1="72" x2="47.93" y2="63" stroke="#64748b" stroke-width="1"/>
<line x1="143.78" y1="72" x2="143.78" y2="63" stroke="#64748b" stroke-width="1"/>
<line x1="239.63" y1="72" x2="239.63" y2="63" stroke="#64748b" stroke-width="1"/>
<!-- 1in major ticks (height=14) with labels — at 1, 2, 3 in -->
<line x1="95.85" y1="72" x2="95.85" y2="58" stroke="#334155" stroke-width="1.25"/>
<text x="95.85" y="64" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">1</text>
<line x1="191.71" y1="72" x2="191.71" y2="58" stroke="#334155" stroke-width="1.25"/>
<text x="191.71" y="64" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">2</text>
<line x1="287.56" y1="72" x2="287.56" y2="58" stroke="#334155" stroke-width="1.25"/>
<text x="287.56" y="64" font-family="sans-serif" font-size="6" fill="#475569" text-anchor="middle">3</text>
<!-- ════════════════════════════════════════════════════ -->
<!-- CLIP SLOT DANGER ZONE markers -->
<!-- Positions retained from original calibration file -->
<!-- ════════════════════════════════════════════════════ -->
<rect x="53" y="22" width="48" height="11" rx="3" fill="#ef4444" fill-opacity="0.35" stroke="#b91c1c" stroke-width="0.75"/>
<rect x="231" y="22" width="48" height="11" rx="3" fill="#ef4444" fill-opacity="0.35" stroke="#b91c1c" stroke-width="0.75"/>
<text x="166" y="39" font-family="monospace" font-size="8.5" fill="#ef4444" text-anchor="middle">CLIP SLOT DANGER ZONE</text>
<!-- 88mm edge tick (right boundary reference) -->
<line x1="330.5" y1="0" x2="330.5" y2="5" stroke="#334155" stroke-width="1.25"/>
<line x1="330.5" y1="72" x2="330.5" y2="67" stroke="#334155" stroke-width="1.25"/>
<!-- ════════════════════════════════════════════════════ -->
<!-- BORDER — drawn last, sits on top of rulers -->
<!-- ════════════════════════════════════════════════════ -->
<rect x="1.5" y="1.5" width="329" height="69" fill="none" stroke="#64748b" stroke-width="3"/>
</svg>

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 13 KiB