diff --git a/src/shipyard-ui.js b/src/shipyard-ui.js
new file mode 100644
index 0000000..0505a53
--- /dev/null
+++ b/src/shipyard-ui.js
@@ -0,0 +1,465 @@
+import titleBannerUrl from './assets/shipyard-ui/title_banner.png?url';
+import shipSelectorUrl from './assets/shipyard-ui/ship_selector.png?url';
+import shipPreviewUrl from './assets/shipyard-ui/ship_preview.png?url';
+import upgradesUrl from './assets/shipyard-ui/upgrades.png?url';
+import activeUrl from './assets/shipyard-ui/active.png?url';
+import closeButtonUrl from './assets/shipyard-ui/close_button.png?url';
+
+const UPGRADE_SLOTS = [
+ { id: 'tuned-propellers', label: 'Engine Lv 1' },
+ { id: 'reinforced-keel', label: 'Hull Plating Lv 1' },
+ { id: 'expanded-hold', label: 'Cargo Hold Lv 1' },
+ { id: 'reinforced-rudder', label: 'Rudder Lv 1' },
+];
+
+const dispatchAction = (action, extra = {}) => {
+ window.dispatchEvent(new CustomEvent('airship:action', {
+ detail: { action, ...extra },
+ }));
+};
+
+const overlay = document.createElement('div');
+overlay.id = 'shipyard-overlay';
+overlay.hidden = true;
+overlay.innerHTML = `
+
+
+
+
+
+ PORT --
+ COIN 0
+ SCRAP 0
+ SALVAGE 0
+
+
+
+
+
+
+

+
+ SPARROW
+ Hull 100
+ Speed 180
+ Firepower 16
+ Handling 76
+ Cargo 4
+
+
+
+
+

+
+ ${UPGRADE_SLOTS.map(({ id, label }) => `
+
+ `).join('')}
+
+
+

+
SPARROW ACTIVE
+
+
+
+
+
+
+`;
+document.body.appendChild(overlay);
+
+const style = document.createElement('style');
+style.textContent = `
+ #shipyard-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 450;
+ color: #f4d99a;
+ font-family: Georgia, 'Times New Roman', serif;
+ touch-action: pan-y;
+ }
+
+ #shipyard-overlay[hidden] { display: none; }
+
+ .shipyard-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(3, 11, 16, .9);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ }
+
+ .shipyard-shell {
+ position: relative;
+ width: min(1180px, calc(100vw - 24px));
+ max-height: calc(100dvh - 20px);
+ margin: max(10px, env(safe-area-inset-top)) auto;
+ padding: 10px;
+ overflow: auto;
+ border: 1px solid rgba(211, 157, 65, .6);
+ border-radius: 14px;
+ background:
+ radial-gradient(circle at 50% 5%, rgba(20, 70, 83, .34), transparent 38%),
+ linear-gradient(180deg, #061b24 0%, #041218 100%);
+ box-shadow: 0 18px 60px rgba(0, 0, 0, .65);
+ overscroll-behavior: contain;
+ }
+
+ .shipyard-header {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ min-height: 70px;
+ }
+
+ .shipyard-title {
+ width: min(595px, 76vw);
+ height: auto;
+ object-fit: contain;
+ }
+
+ .shipyard-close {
+ position: absolute;
+ top: 0;
+ right: max(0px, env(safe-area-inset-right));
+ width: clamp(48px, 7vw, 72px);
+ aspect-ratio: 1;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+ touch-action: manipulation;
+ }
+
+ .shipyard-close img { width: 100%; height: 100%; object-fit: contain; }
+
+ .shipyard-resources {
+ display: flex;
+ gap: 8px;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin: 4px auto 10px;
+ font-size: clamp(11px, 1.6vw, 15px);
+ }
+
+ .shipyard-resources span {
+ min-width: 120px;
+ padding: 7px 12px;
+ border: 1px solid rgba(198, 145, 59, .42);
+ border-radius: 999px;
+ background: rgba(3, 20, 27, .88);
+ text-align: center;
+ }
+
+ .shipyard-resources strong { color: #fff0bd; }
+
+ .shipyard-grid {
+ display: grid;
+ grid-template-columns: minmax(170px, .72fr) minmax(260px, 1fr) minmax(360px, 1.2fr);
+ gap: 10px;
+ align-items: start;
+ }
+
+ .shipyard-selector img,
+ .shipyard-preview,
+ .shipyard-upgrades {
+ display: block;
+ width: 100%;
+ height: auto;
+ user-select: none;
+ -webkit-user-select: none;
+ }
+
+ .shipyard-preview-wrap,
+ .shipyard-upgrade-wrap { position: relative; }
+
+ .shipyard-live-stats {
+ position: absolute;
+ left: 5%;
+ right: 5%;
+ bottom: 3%;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 4px 10px;
+ padding: 8px 10px;
+ border: 1px solid rgba(213, 162, 76, .48);
+ border-radius: 8px;
+ background: rgba(3, 18, 24, .86);
+ font-size: clamp(10px, 1.25vw, 13px);
+ text-shadow: 0 1px 2px #000;
+ }
+
+ .shipyard-live-stats strong {
+ grid-column: 1 / -1;
+ color: #fff0bd;
+ letter-spacing: .12em;
+ }
+
+ .shipyard-upgrade-hotspots {
+ position: absolute;
+ left: 3.5%;
+ right: 3.5%;
+ top: 18%;
+ bottom: 10%;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 2.5%;
+ }
+
+ .shipyard-upgrade-button {
+ position: relative;
+ border: 0;
+ border-radius: 8px;
+ background: transparent;
+ cursor: pointer;
+ touch-action: manipulation;
+ }
+
+ .shipyard-upgrade-button:not(:disabled):active { background: rgba(255, 214, 120, .12); }
+ .shipyard-upgrade-button:disabled { cursor: default; }
+
+ .shipyard-upgrade-state {
+ position: absolute;
+ left: 50%;
+ bottom: 3px;
+ transform: translateX(-50%);
+ padding: 2px 5px;
+ border-radius: 999px;
+ background: rgba(4, 16, 21, .88);
+ color: #ffe4a1;
+ font: 700 clamp(8px, 1vw, 11px)/1 system-ui, sans-serif;
+ white-space: nowrap;
+ }
+
+ .shipyard-upgrade-button.installed .shipyard-upgrade-state { color: #aef0b8; }
+ .shipyard-upgrade-button.locked .shipyard-upgrade-state { color: #d8a8a0; }
+
+ .shipyard-active-plate {
+ position: absolute;
+ left: 50%;
+ bottom: -58px;
+ width: min(255px, 55%);
+ transform: translateX(-50%);
+ }
+
+ .shipyard-active-plate img { display: block; width: 100%; }
+ .shipyard-active-plate span {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ color: #b9efc0;
+ font-size: clamp(9px, 1.15vw, 13px);
+ letter-spacing: .08em;
+ }
+
+ .shipyard-footer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ min-height: 72px;
+ margin-top: 56px;
+ }
+
+ .shipyard-salvage {
+ min-width: 150px;
+ min-height: 44px;
+ padding: 7px 14px;
+ border: 1px solid #b88131;
+ border-radius: 9px;
+ background: linear-gradient(#174e43, #0d302a);
+ color: #e6f5d0;
+ font: 700 12px/1.15 Georgia, serif;
+ touch-action: manipulation;
+ }
+
+ .shipyard-salvage:disabled { opacity: .35; }
+
+ .shipyard-message {
+ min-height: 1.2em;
+ max-width: 50%;
+ color: #fff0bd;
+ font-size: 12px;
+ }
+
+ body.shipyard-open #mobile-controls { visibility: hidden; }
+
+ @media (max-width: 760px), (max-height: 520px) {
+ .shipyard-shell {
+ width: calc(100vw - 8px);
+ max-height: calc(100dvh - 8px);
+ margin: 4px auto;
+ padding: 6px;
+ border-radius: 8px;
+ }
+
+ .shipyard-header { min-height: 46px; }
+ .shipyard-title { width: min(460px, 68vw); }
+ .shipyard-close { width: 46px; }
+
+ .shipyard-resources { gap: 4px; margin: 2px 0 6px; }
+ .shipyard-resources span { min-width: 0; padding: 4px 7px; font-size: 10px; }
+
+ .shipyard-grid {
+ grid-template-columns: minmax(150px, .8fr) minmax(330px, 1.55fr);
+ gap: 6px;
+ }
+
+ .shipyard-selector { display: none; }
+
+ .shipyard-preview-wrap { max-height: 64dvh; overflow: hidden; }
+ .shipyard-preview {
+ height: min(64dvh, 430px);
+ width: 100%;
+ object-fit: cover;
+ object-position: center;
+ }
+
+ .shipyard-live-stats {
+ left: 3%;
+ right: 3%;
+ bottom: 2%;
+ grid-template-columns: 1fr;
+ gap: 2px;
+ padding: 5px 7px;
+ font-size: 9px;
+ }
+
+ .shipyard-upgrade-wrap { align-self: center; }
+ .shipyard-active-plate { bottom: -46px; width: 45%; }
+
+ .shipyard-footer {
+ min-height: 54px;
+ margin-top: 46px;
+ gap: 7px;
+ }
+
+ .shipyard-salvage { min-width: 120px; min-height: 40px; font-size: 10px; }
+ .shipyard-message { max-width: 56%; font-size: 10px; }
+ }
+
+ @media (orientation: portrait) and (max-width: 760px) {
+ .shipyard-title { width: min(500px, 80vw); }
+ .shipyard-grid { grid-template-columns: 1fr; }
+ .shipyard-preview-wrap { display: none; }
+ .shipyard-upgrade-wrap { width: min(96vw, 520px); margin: 0 auto; }
+ .shipyard-footer { margin-top: 48px; }
+ }
+`;
+document.head.appendChild(style);
+
+const fields = Object.fromEntries(
+ [...overlay.querySelectorAll('[data-shipyard]')].map((el) => [el.dataset.shipyard, el]),
+);
+const closeButton = overlay.querySelector('.shipyard-close');
+const fitButton = overlay.querySelector('.shipyard-salvage');
+const upgradeButtons = [...overlay.querySelectorAll('.shipyard-upgrade-button')];
+
+let latestState = null;
+
+const findStock = (id) =>
+ latestState?.progression?.marketStock?.find?.((module) => module.id === id) || null;
+
+const render = (state) => {
+ if (!state?.progression) return;
+ latestState = state;
+
+ const progression = state.progression;
+ const stats = progression.shipStats || {};
+ const loadout = progression.loadout || {};
+
+ fields.port.textContent = progression.nearestPort || '--';
+ fields.credits.textContent = progression.credits ?? 0;
+ fields.scrap.textContent = progression.scrap ?? 0;
+ fields.inventory.textContent = progression.inventoryCount ?? 0;
+ fields.hull.textContent = `Hull ${stats.maxHull ?? 100}`;
+ fields.speed.textContent = `Speed ${stats.maxSpeed ?? 180}`;
+ fields.firepower.textContent = `Firepower ${stats.cannonDamage ?? 16}`;
+ fields.handling.textContent = `Handling ${stats.turnRate ?? 76}`;
+ fields.cargo.textContent = `Cargo ${stats.cargoCapacity ?? 4}`;
+
+ for (const button of upgradeButtons) {
+ const id = button.dataset.moduleId;
+ const stock = findStock(id);
+ const installed = Object.values(loadout).includes(id);
+ const stateLabel = button.querySelector('.shipyard-upgrade-state');
+
+ button.classList.toggle('installed', installed);
+ button.classList.toggle('locked', !stock && !installed);
+ button.disabled = installed || !stock || progression.credits < (stock?.price || Infinity);
+
+ stateLabel.textContent = installed
+ ? 'INSTALLED'
+ : !stock
+ ? 'NOT STOCKED'
+ : progression.credits < stock.price
+ ? `NEED ยข${stock.price}`
+ : 'TAP TO BUY';
+ }
+
+ const salvage = progression.salvageModule;
+ fitButton.disabled = !salvage;
+ fitButton.textContent = salvage ? `FIT ${salvage.name}` : 'FIT SALVAGE';
+ fields.message.textContent = String(state.status || '').replace(/\n/g, ' ');
+};
+
+const show = (state) => {
+ render(state);
+ overlay.hidden = false;
+ document.body.classList.add('shipyard-open');
+ closeButton.focus({ preventScroll: true });
+};
+
+const hide = () => {
+ overlay.hidden = true;
+ document.body.classList.remove('shipyard-open');
+};
+
+closeButton.addEventListener('click', () => {
+ dispatchAction('close-shipyard');
+ hide();
+});
+
+overlay.querySelector('.shipyard-backdrop').addEventListener('click', () => {
+ dispatchAction('close-shipyard');
+ hide();
+});
+
+for (const button of upgradeButtons) {
+ button.addEventListener('click', () => {
+ const moduleId = button.dataset.moduleId;
+ if (!moduleId || button.disabled) return;
+ dispatchAction('purchase-module', { moduleId });
+ });
+}
+
+fitButton.addEventListener('click', () => {
+ if (fitButton.disabled) return;
+ dispatchAction('fit-salvage');
+});
+
+window.addEventListener('airship:shipyard-open', (event) => show(event.detail));
+window.addEventListener('airship:shipyard-state', (event) => render(event.detail));
+window.addEventListener('airship:shipyard-close', hide);
+window.addEventListener('airship:hud-state', (event) => {
+ latestState = event.detail;
+ if (!overlay.hidden) render(event.detail);
+});
+window.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape' && !overlay.hidden) {
+ dispatchAction('close-shipyard');
+ hide();
+ }
+});