const pptxgen = require("pptxgenjs"); const React = require("react"); const ReactDOMServer = require("react-dom/server"); const sharp = require("sharp"); const { FaSearch, FaClipboardList, FaExclamationTriangle, FaLayerGroup, FaTree, FaIndustry, FaTint, FaQuestionCircle, FaBullseye, FaCamera, FaCheckCircle, FaSyncAlt, FaShieldAlt, FaExchangeAlt, FaHistory, FaRocket, FaChartLine, FaBuilding, FaLink, FaDatabase } = require("react-icons/fa"); // Palett: asfalt + galvstål + signalgul (vägmarkering) const DARK = "23272B", CARD_D = "2E343B", INK = "2A3138", BODY = "3A4149"; const MUTED = "6B7480", MUTED_L = "AEB6BF", LIGHT = "F3F5F7", WHITE = "FFFFFF"; const YEL = "F5B301", YEL_TINT = "FFF6DC", LINE = "D9DEE4"; const H = "Arial", B = "Calibri", MONO = "Courier New"; const mkShadow = () => ({ type: "outer", color: "000000", blur: 7, offset: 2, angle: 45, opacity: 0.14 }); async function icon(Comp, color, size = 256) { const svg = ReactDOMServer.renderToStaticMarkup(React.createElement(Comp, { color, size: String(size) })); const png = await sharp(Buffer.from(svg)).png().toBuffer(); return "image/png;base64," + png.toString("base64"); } (async () => { const P = {}; P.search = await icon(FaSearch, "#" + INK); P.clip = await icon(FaClipboardList, "#" + INK); P.warn = await icon(FaExclamationTriangle, "#" + INK); P.layer = await icon(FaLayerGroup, "#" + INK); P.tree = await icon(FaTree, "#" + INK); P.ind = await icon(FaIndustry, "#" + INK); P.tint = await icon(FaTint, "#" + INK); P.q = await icon(FaQuestionCircle, "#" + WHITE); P.bull = await icon(FaBullseye, "#" + WHITE); P.cam = await icon(FaCamera, "#" + WHITE); P.check = await icon(FaCheckCircle, "#" + WHITE); P.sync = await icon(FaSyncAlt, "#" + INK); P.shield = await icon(FaShieldAlt, "#" + INK); P.exch = await icon(FaExchangeAlt, "#" + INK); P.hist = await icon(FaHistory, "#" + INK); P.rocket = await icon(FaRocket, "#" + INK); P.chart = await icon(FaChartLine, "#" + INK); P.build = await icon(FaBuilding, "#" + INK); P.link = await icon(FaLink, "#" + INK); P.db = await icon(FaDatabase, "#" + INK); P.clipY = await icon(FaClipboardList, "#" + INK); P.warnY = await icon(FaExclamationTriangle, "#" + INK); P.checkY = await icon(FaCheckCircle, "#" + INK); const pres = new pptxgen(); pres.layout = "LAYOUT_16x9"; pres.author = "Landvex"; pres.title = "Landvex — Infrastructure Object Library, pilotpitch"; const title = (s, t, dark = false) => s.addText(t, { x: 0.6, y: 0.42, w: 8.8, h: 0.7, fontFace: H, fontSize: 36, bold: true, color: dark ? WHITE : INK, margin: 0 }); const circleIcon = (s, data, cx, cy, d, fill) => { s.addShape(pres.shapes.OVAL, { x: cx, y: cy, w: d, h: d, fill: { color: fill } }); const pad = d * 0.26; s.addImage({ data, x: cx + pad, y: cy + pad, w: d - 2 * pad, h: d - 2 * pad }); }; // ---------- S1 · Titel (mörk) ---------- let s = pres.addSlide(); s.background = { color: DARK }; s.addText("LANDVEX × QUIXZOOM", { x: 0.6, y: 0.55, w: 5.8, h: 0.35, fontFace: H, fontSize: 13, bold: true, color: YEL, charSpacing: 4, margin: 0 }); s.addText([ { text: "Infrastructure", options: { breakLine: true } }, { text: "Object Library" } ], { x: 0.6, y: 1.05, w: 5.9, h: 1.8, fontFace: H, fontSize: 42, bold: true, color: WHITE, margin: 0, lineSpacing: 46 }); s.addText("Vet vad som står där ute — varje objekt i gaturummet, med källa på varje faktum.", { x: 0.6, y: 2.95, w: 5.5, h: 0.75, fontFace: B, fontSize: 15, color: MUTED_L, margin: 0 }); s.addText("37 klasser · 26 modeller · 167 grafkanter · 0 valideringsfel", { x: 0.6, y: 3.85, w: 5.7, h: 0.35, fontFace: H, fontSize: 13, bold: true, color: YEL, margin: 0 }); s.addText("Pilotpitch · juli 2026", { x: 0.6, y: 5.05, w: 4, h: 0.3, fontFace: B, fontSize: 11, color: MUTED, margin: 0 }); const chips = [ ["LVX-TRP-0102", "Vägbelysningsarmatur"], ["LVX-P-0000026", "Tesla Supercharger V4"], ["LVX-VAT-0103", "Brandpost / hydrant"], ["LVX-ELN-0101", "Kabelskåp lågspänning"] ]; chips.forEach((c, i) => { const y = 1.05 + i * 0.82; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 6.35, y, w: 3.05, h: 0.62, rectRadius: 0.06, fill: { color: CARD_D } }); s.addText([ { text: c[0], options: { fontFace: MONO, fontSize: 11.5, bold: true, color: YEL, breakLine: true } }, { text: c[1], options: { fontFace: B, fontSize: 10.5, color: WHITE } } ], { x: 6.55, y: y + 0.05, w: 2.75, h: 0.52, margin: 0, lineSpacing: 13 }); }); s.addNotes("Landvex bygger referensbiblioteket för fysisk infrastruktur. QUIXZOOM är fältarmen. Siffrorna är dagens bibliotek — validerat, inte vision."); // ---------- S2 · Problemet ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Gatan saknar identitet"); const rows2 = [ [P.clip, "Inventering är manuell och dyr", "Beståndsdata byggs om från noll vid varje upphandling och ägarbyte."], [P.warn, "Utgångna modeller upptäcks för sent", "Reservdelsrisken syns först när armaturen slocknat eller locket spruckit."], [P.search, "\u201dVad är detta?\u201d saknar svar i fält", "Tekniker, system och AI gissar — det finns ingen gemensam referens."] ]; rows2.forEach((r, i) => { const y = 1.4 + i * 1.15; circleIcon(s, r[0], 0.6, y, 0.52, YEL); s.addText(r[1], { x: 1.35, y: y - 0.06, w: 4.15, h: 0.35, fontFace: H, fontSize: 15, bold: true, color: INK, margin: 0 }); s.addText(r[2], { x: 1.35, y: y + 0.3, w: 4.15, h: 0.62, fontFace: B, fontSize: 12, color: MUTED, margin: 0 }); }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.35, w: 3.55, h: 3.4, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() }); s.addText("60M+", { x: 6.1, y: 1.6, w: 3.05, h: 0.95, fontFace: H, fontSize: 58, bold: true, color: INK, margin: 0 }); s.addText("elstolpar enbart i USA — de flesta bär identiteten inbränd i godset.", { x: 6.1, y: 2.65, w: 3.05, h: 0.85, fontFace: B, fontSize: 12.5, color: MUTED, margin: 0 }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 6.1, y: 3.7, w: 3.05, h: 0.62, rectRadius: 0.06, fill: { color: YEL_TINT } }); s.addText("birthmark: år · klass · impregnering", { x: 6.28, y: 3.78, w: 2.75, h: 0.46, fontFace: MONO, fontSize: 10.5, bold: true, color: INK, margin: 0, valign: "middle" }); s.addNotes("Problemet är inte brist på data — det är brist på identitet. Ingen gemensam nyckel mellan fält, system och leverantörer."); // ---------- S3 · Lösningen: fem nivåer ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Ett bibliotek — fem nivåer"); const steps = [ ["Domän", "TRP · VAT · ELN…"], ["Objektklass", "LVX-TRP-0102"], ["Typ", "Cobrahead-LED"], ["Produktmodell", "LVX-P-0000003"], ["Individ", "LVX-I-… (pilot)"] ]; steps.forEach((st, i) => { const x = 0.6 + i * 1.9; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.35, w: 1.55, h: 0.95, rectRadius: 0.08, fill: { color: LIGHT } }); s.addText([ { text: st[0], options: { fontFace: H, fontSize: 12.5, bold: true, color: INK, breakLine: true } }, { text: st[1], options: { fontFace: MONO, fontSize: 9.5, color: MUTED } } ], { x: x + 0.12, y: 1.47, w: 1.31, h: 0.75, margin: 0, lineSpacing: 14 }); if (i < 4) s.addText("\u203A", { x: x + 1.55, y: 1.55, w: 0.35, h: 0.5, fontFace: H, fontSize: 20, color: MUTED, align: "center", margin: 0 }); }); const cards3 = [ [P.link, "Permanenta LVX-ID", "Identiteten överlever ägarbyten, versioner och systembyten — referensen alla kan peka på."], [P.db, "Källa på varje faktum", "Problem utan källa publiceras aldrig. PLATSHÅLLARE-regeln är hård — juridik by design."], [P.hist, "Bitemporal fakta", "Vad som gällde där ute — och när vi visste det. Ersättningskedjor blir spårbara."] ]; cards3.forEach((c, i) => { const x = 0.6 + i * 3.07; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 2.85, w: 2.87, h: 2.15, rectRadius: 0.09, fill: { color: LIGHT } }); circleIcon(s, c[0], x + 0.22, y3(i), 0.46, YEL); s.addText(c[1], { x: x + 0.85, y: 3.05, w: 1.95, h: 0.6, fontFace: H, fontSize: 13.5, bold: true, color: INK, margin: 0 }); s.addText(c[2], { x: x + 0.22, y: 3.75, w: 2.45, h: 1.1, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 }); }); function y3() { return 3.05; } s.addNotes("Fem nivåer: domän ned till individ. Piloten fyller individlagret för kundens geografi. ID:na är permanenta — det är vallgraven."); // ---------- S4 · Bevis ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Byggt — och validerat"); const stats = [ ["37", "objektklasser — 31 källbelagda"], ["26", "produktmodeller — alla källbelagda"], ["167", "grafkanter i konsolidering v2"], ["0", "valideringsfel mot schemat"] ]; stats.forEach((st, i) => { const x = 0.6 + i * 2.25; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.05, h: 1.45, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() }); s.addText(st[0], { x: x + 0.18, y: 1.42, w: 1.7, h: 0.7, fontFace: H, fontSize: 40, bold: true, color: i === 3 ? YEL : INK, margin: 0 }); s.addText(st[1], { x: x + 0.18, y: 2.14, w: 1.72, h: 0.55, fontFace: B, fontSize: 10.5, color: MUTED, margin: 0 }); }); s.addText("Verifieringstrappan — varje fältfoto lyfter posten ett steg", { x: 0.6, y: 3.12, w: 8.8, h: 0.3, fontFace: B, fontSize: 12.5, italic: true, color: MUTED, margin: 0 }); const stair = ["Obekräftad", "Källbelagd", "Fältverifierad", "Tillverkarbekräftad"]; stair.forEach((t, i) => { const h = 0.55 + i * 0.28, x = 0.6 + i * 2.25; s.addShape(pres.shapes.RECTANGLE, { x, y: 5.05 - h, w: 2.05, h, fill: { color: i === 3 ? YEL : ["E7EAEE", "D5DAE0", "BFC7CF"][i] } }); s.addText(t, { x: x + 0.1, y: 5.05 - h + 0.08, w: 1.85, h: 0.35, fontFace: H, fontSize: 11.5, bold: true, color: INK, margin: 0 }); }); s.addNotes("Detta är inte en idé — biblioteket finns, är källbelagt och maskinvaliderat. Trappan är kvalitetsmodellen: piloten flyttar poster till fältverifierad."); // ---------- S5 · OCR-guldet ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Identiteten sitter i godset"); const cards5 = [ [P.tree, "Stolpens birthmark", "Inbränd märkning i ögonhöjd: år, längd, klass och impregneringsmedel.", "år · klass · CCA/penta"], [P.ind, "Gjutmärket", "Tillverkare, funktion och lastklass gjuts in i lock och ram.", "ULEFOS · EN124-2 · D400"], [P.tint, "Hydrantens kropp", "AWWA kräver gjuten märkning — tillverkningsår och öppningsriktning.", "AMERICAN-DARLING · 1998"] ]; cards5.forEach((c, i) => { const x = 0.6 + i * 3.07; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.15, rectRadius: 0.09, fill: { color: LIGHT } }); circleIcon(s, c[0], x + 0.22, 1.55, 0.5, YEL); s.addText(c[1], { x: x + 0.88, y: 1.6, w: 1.95, h: 0.6, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 }); s.addText(c[2], { x: x + 0.22, y: 2.35, w: 2.45, h: 1.05, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: x + 0.22, y: 3.6, w: 2.43, h: 0.55, rectRadius: 0.06, fill: { color: WHITE }, line: { color: LINE, width: 1 } }); s.addText(c[3], { x: x + 0.36, y: 3.66, w: 2.2, h: 0.43, fontFace: MONO, fontSize: 10, bold: true, color: INK, margin: 0, valign: "middle" }); }); s.addText("Ingen databas krävs på plats — objektet bär sin egen identitet. OCR-först är protokollets kärna.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 }); s.addNotes("Läsbarheten är affärsidén: birthmarks, gjutmärken och typskyltar gör varje foto till en databaspost."); // ---------- S6 · Flyhjulet ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Flyhjulet: QUIXZOOM"); const nodes = [ [2.3, 1.12, P.q, "Kundfråga", "identify → okänd"], [3.85, 2.38, P.bull, "Bounty", "skapas automatiskt"], [2.3, 3.6, P.cam, "Zoomer-foto", "OCR + position"], [0.75, 2.38, P.check, "Fältverifierad", "post + individ"] ]; nodes.forEach(n => { circleIcon(s, n[2], n[0], n[1], 0.95, DARK); s.addText(n[3], { x: n[0] - 0.45, y: n[1] + 0.92, w: 1.85, h: 0.28, fontFace: H, fontSize: 11.5, bold: true, color: INK, align: "center", margin: 0 }); s.addText(n[4], { x: n[0] - 0.45, y: n[1] + 1.16, w: 1.85, h: 0.24, fontFace: B, fontSize: 9.5, color: MUTED, align: "center", margin: 0 }); }); circleIcon(s, P.sync, 2.5, 2.6, 0.55, YEL); s.addText("\u2198", { x: 3.5, y: 2.0, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 }); s.addText("\u2199", { x: 3.5, y: 3.3, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 }); s.addText("\u2196", { x: 1.85, y: 3.3, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 }); s.addText("\u2197", { x: 1.85, y: 2.0, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.3, w: 3.55, h: 3.2, rectRadius: 0.09, fill: { color: LIGHT } }); s.addText("Bounty-motorn", { x: 6.1, y: 1.5, w: 3.05, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 }); s.addText([ { text: "16 aktiva bounties ur posternas OCR-fält", options: { bullet: true, breakLine: true } }, { text: "Poäng: bas · först-på-individ ×2 · full OCR +50 %", options: { bullet: true, breakLine: true } }, { text: "Hårda säkerhetsregler — allt fotas från gångyta", options: { bullet: true, breakLine: true } }, { text: "Varje \u201dobekräftad\u201d i API:t skapar en bounty", options: { bullet: true } } ], { x: 6.1, y: 1.95, w: 3.1, h: 2.4, fontFace: B, fontSize: 11.5, color: BODY, paraSpaceAfter: 8, margin: 0 }); s.addText("Kundens fråga finansierar datan som gör nästa svar bättre.", { x: 0.6, y: 5.12, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 }); s.addNotes("Flyhjulet: obesvarade kundfrågor blir bounties, Zoomers fotograferar, posten fältverifieras, svaret blir bättre. Andelen kunddrivna bounties är hälsotalet."); // ---------- S7 · API:t ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Produkten: Object API v0"); const eps = [ ["POST", "/v0/identify", "bild in → modell ut"], ["GET", "/v0/objects/{lvx_id}", "hela posten + källor"], ["GET", "/v0/objects/{id}/succession", "ersättningskedjan"], ["GET", "/v0/search", "fritext + filter"], ["GET", "/v0/standards/{beteckning}", "objekt per standard"], ["POST", "/v0/feedback", "fel/okänt → bounty"] ]; eps.forEach((e, i) => { const y = 1.3 + i * 0.58; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 0.6, y, w: 0.62, h: 0.32, rectRadius: 0.05, fill: { color: e[0] === "POST" ? YEL : CARD_D } }); s.addText(e[0], { x: 0.6, y, w: 0.62, h: 0.32, fontFace: H, fontSize: 9, bold: true, color: e[0] === "POST" ? INK : WHITE, align: "center", valign: "middle", margin: 0 }); s.addText(e[1], { x: 1.35, y: y + 0.01, w: 2.62, h: 0.32, fontFace: MONO, fontSize: 10.5, bold: true, color: INK, margin: 0, valign: "middle" }); s.addText(e[2], { x: 4.02, y: y + 0.01, w: 1.72, h: 0.32, fontFace: B, fontSize: 9.5, color: MUTED, margin: 0, valign: "middle" }); }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.3, w: 3.55, h: 3.45, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() }); circleIcon(s, P.shield, 6.1, 1.52, 0.46, YEL); s.addText("Svarskontraktet", { x: 6.72, y: 1.58, w: 2.6, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 }); s.addText([ { text: "verifieringsnivå — alltid med", options: { bullet: true, breakLine: true } }, { text: "konfidens 0–1 — aldrig dold", options: { bullet: true, breakLine: true } }, { text: "källor[] på varje påstående", options: { bullet: true, breakLine: true } }, { text: "PLATSHÅLLARE filtreras bort vid serialisering", options: { bullet: true, breakLine: true } }, { text: "svagt svar ⇒ bounty-ID följer med", options: { bullet: true } } ], { x: 6.1, y: 2.15, w: 3.1, h: 2.4, fontFace: B, fontSize: 11.5, color: BODY, paraSpaceAfter: 8, margin: 0 }); s.addText("Juridiken sitter i kontraktet — referenser och beteckningar, aldrig standardtext.", { x: 0.6, y: 5.05, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 }); s.addNotes("Sex endpoints räcker för piloten. Kontraktet är differentieringen: ärlighet och proveniens är obligatoriska fält, inte fotnoter."); // ---------- S8 · Golden cases ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Tre svar som säljer"); const g = [ [P.exch, "Ersättningskedjan", "GE ERS identifieras → status: ersatt → ersättare: ERL-serien. Reservdelsrisken syns innan lagret tar slut.", "identify → succession"], [P.hist, "Årsmodell ur godset", "American-Darling B-84-B, gjutet 1998, öppnar vänster — individdata utan register.", "konfidens 0,97 · källbelagd"], [P.bull2 || P.search, "Ärlig när den inte vet", "Okänt UK-skåp → bästa klass, konfidens 0,71 — och en bounty skapas automatiskt.", "svar + BNTY-A0001"] ]; g.forEach((c, i) => { const x = 0.6 + i * 3.07; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.15, rectRadius: 0.09, fill: { color: LIGHT } }); circleIcon(s, c[0], x + 0.22, 1.55, 0.5, YEL); s.addText(c[1], { x: x + 0.88, y: 1.6, w: 1.95, h: 0.6, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 }); s.addText(c[2], { x: x + 0.22, y: 2.35, w: 2.45, h: 1.15, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: x + 0.22, y: 3.6, w: 2.43, h: 0.55, rectRadius: 0.06, fill: { color: WHITE }, line: { color: LINE, width: 1 } }); s.addText(c[3], { x: x + 0.36, y: 3.66, w: 2.2, h: 0.43, fontFace: MONO, fontSize: 10, bold: true, color: INK, margin: 0, valign: "middle" }); }); s.addText("Alla tre körs på riktiga poster — demodatat ingår i pilotpaketet.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 }); s.addNotes("Tre golden cases som demoas live: succession, individdata ur gjutgods, och ärlighet + bounty vid okänt objekt."); // ---------- S9 · Paketering ---------- s = pres.addSlide(); s.background = { color: WHITE }; title(s, "Paketering"); const pk = [ [P.rocket, "Pilot", "1–3 designpartners", ["Fast månadsavgift, generös kvot", "Gemensam backlog styr utvecklingen", "Betalar även med fältaccess"], LIGHT], [P.chart, "Pro", "entreprenad · besiktning · försäkring", ["Usage-based per identify", "Billiga uppslag driver LVX-ID-inlåsning", "Volymtrappa"], YEL_TINT], [P.build, "Enterprise", "nätbolag · förvaltare · tillverkare", ["SLA + eget individlager (LVX-I)", "Bulkexport och integrationer", "Riktade bounties i egen geografi"], LIGHT] ]; pk.forEach((c, i) => { const x = 0.6 + i * 3.07; s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.1, rectRadius: 0.09, fill: { color: c[4] }, shadow: i === 1 ? mkShadow() : undefined }); circleIcon(s, c[0], x + 0.22, 1.52, 0.46, WHITE === c[4] ? LIGHT : WHITE); s.addText(c[1], { x: x + 0.82, y: 1.55, w: 1.95, h: 0.35, fontFace: H, fontSize: 16, bold: true, color: INK, margin: 0 }); s.addText(c[2], { x: x + 0.22, y: 2.12, w: 2.45, h: 0.3, fontFace: B, fontSize: 10.5, italic: true, color: MUTED, margin: 0 }); s.addText(c[3].map((t, j) => ({ text: t, options: { bullet: true, breakLine: j < 2 } })), { x: x + 0.22, y: 2.5, w: 2.5, h: 1.75, fontFace: B, fontSize: 11, color: BODY, paraSpaceAfter: 7, margin: 0 }); }); s.addText("Identifieringen är värdet · uppslaget är räckvidden · bounty-krediter köper fältverifiering där kunden behöver den.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 12.5, italic: true, color: MUTED, margin: 0 }); s.addNotes("SaaS-logiken: identify prissätts per värdefullt svar, uppslag hålls billiga för att göra LVX-ID till branschreferens, Enterprise får eget individlager."); // ---------- S10 · Piloten (mörk) ---------- s = pres.addSlide(); s.background = { color: DARK }; title(s, "Piloten: 90 dagar till bevis", true); const uc = [ [P.clipY, "Inventering", "Fota beståndet → klass, modell och status per objekt."], [P.warnY, "Utgånget-listan", "Allt med status ersatt/utgången — med ersättare och källa."], [P.checkY, "Problemflagg", "Källbelagda klassproblem matchade mot ert bestånd."] ]; uc.forEach((r, i) => { const y = 1.45 + i * 1.1; circleIcon(s, r[0], 0.6, y, 0.52, YEL); s.addText(r[1], { x: 1.35, y: y - 0.05, w: 4.1, h: 0.35, fontFace: H, fontSize: 15, bold: true, color: WHITE, margin: 0 }); s.addText(r[2], { x: 1.35, y: y + 0.31, w: 4.1, h: 0.6, fontFace: B, fontSize: 12, color: MUTED_L, margin: 0 }); }); s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.35, w: 3.55, h: 3.35, rectRadius: 0.09, fill: { color: CARD_D } }); s.addText("Erbjudandet", { x: 6.1, y: 1.55, w: 3.05, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: WHITE, margin: 0 }); s.addText([ { text: "Avgränsat område, 90 dagar", options: { bullet: true, breakLine: true } }, { text: "Fast pilotavgift — kvittas mot årsavtal", options: { bullet: true, breakLine: true } }, { text: "Gemensam backlog styr bounties", options: { bullet: true, breakLine: true } }, { text: "Ni behåller export av ert individlager", options: { bullet: true } } ], { x: 6.1, y: 2.0, w: 3.1, h: 1.85, fontFace: B, fontSize: 11.5, color: MUTED_L, paraSpaceAfter: 8, margin: 0 }); s.addText("Boka pilotworkshop \u2192", { x: 6.1, y: 4.05, w: 3.05, h: 0.45, fontFace: H, fontSize: 17, bold: true, color: YEL, margin: 0 }); s.addText("LANDVEX × QUIXZOOM · juli 2026", { x: 0.6, y: 5.08, w: 5, h: 0.3, fontFace: B, fontSize: 11, color: MUTED, charSpacing: 2, margin: 0 }); s.addNotes("Stäng med erbjudandet: 90 dagar, avgränsat område, tre användningsfall, fast avgift som kvittas. Nästa steg är en workshop där området och backloggen väljs."); await pres.writeFile({ fileName: "/home/claude/landvex-pilotpitch.pptx" }); console.log("deck skriven"); })().catch(e => { console.error(e); process.exit(1); });