Commit de9ab004 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(proposal): Pro Max screenshots, the full feature list, and a real A4 print

The app screenshots came from a 16:9 emulator, so every phone in the
proposal looked squat. They are recaptured on a 1320x2868 @3x emulator — the
iPhone 16 Pro Max's own 440x956pt screen — so the frame now reads as a
modern phone.

The scope section listed what was out of scope, which read as the offer not
covering the club. It is replaced by an inventory of what is built and
working today, checked against both repos: 52 member-app features and 45
across the portal and the gate scanner, grouped into cards, under the four
pillars and the counts (51 app screens, 34 portal sections, 33 wizards,
6 roles). Items the guide implies but the app has no screen for — uploading
a transfer proof, requesting a plan, autopay — are left out.

Print is now true A4. One print stylesheet serves both the browser's Print
and generate-pdf.mjs: printer-safe margins, a cover page, dark bands printed
light, grids held at their desktop columns, every app and portal screenshot
in captioned galleries built into the page, page numbers in the footer, and
breaks that keep sections whole. 17 pages.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 969b2048
...@@ -591,30 +591,255 @@ footer > *{ position:relative; z-index:1; } ...@@ -591,30 +591,255 @@ footer > *{ position:relative; z-index:1; }
.shot-note h4{ color:var(--navy); font-size:1rem; margin-bottom:6px; } .shot-note h4{ color:var(--navy); font-size:1rem; margin-bottom:6px; }
.shot-note p{ font-size:.88rem; color:var(--txt-2); line-height:1.85; } .shot-note p{ font-size:.88rem; color:var(--txt-2); line-height:1.85; }
/* ================= PRINT / PDF ================= /* ================= FEATURES =================
The page is built for scrolling: reveals start invisible, the nav is fixed, The inventory of what is built. A dark band like the cover: the stats say
and the prototypes show one screen at a time. Print needs the opposite — how much, the four pillars say why, and the chip cards list everything. */
everything visible, nothing floating, and sections that start on a fresh .feat{
page instead of splitting a card in half. */ background:
@media print{ radial-gradient(900px 480px at 88% -6%, rgba(201,162,39,.16), transparent 60%),
.nav, .progress, .proto-tools, .btn-row, .hero-logo{ display:none !important; } radial-gradient(820px 520px at 4% 106%, rgba(30,90,170,.28), transparent 62%),
.reveal{ opacity:1 !important; transform:none !important; } linear-gradient(170deg,#07142B 0%,#0B2144 58%,#07142B 100%);
overflow:hidden;
}
.feat::after{
content:''; position:absolute; inset:0; pointer-events:none; opacity:.08;
background-image:linear-gradient(rgba(255,255,255,.07) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.07) 1px,transparent 1px);
background-size:56px 56px;
mask-image:linear-gradient(180deg,#000,transparent 55%);
-webkit-mask-image:linear-gradient(180deg,#000,transparent 55%);
}
.feat > .wrap{ position:relative; z-index:1; }
.feat-head .lede{ max-width:760px; }
.feat-stats{
display:grid; grid-template-columns:repeat(4,1fr); margin-top:40px;
border:1px solid rgba(255,255,255,.1); border-radius:var(--r-l);
background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,.015));
}
.feat-stats > div{ padding:26px 18px 24px; text-align:center; }
.feat-stats > div + div{ border-inline-start:1px solid rgba(255,255,255,.08); }
.feat-stats b{
display:block; font-family:var(--f-head); font-weight:900; line-height:1;
font-size:clamp(2.3rem,4.6vw,3.3rem); color:var(--gold-2);
}
.feat-stats span{ display:block; margin-top:10px; font-size:.84rem; color:#9DB2CF; }
.pillars{ display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-top:18px; }
.pillar{
padding:22px 20px; border-radius:var(--r-m);
background:linear-gradient(180deg,rgba(201,162,39,.11),rgba(201,162,39,.02) 70%);
border:1px solid rgba(201,162,39,.24);
}
.pillar-n{
width:30px; height:30px; border-radius:9px; display:grid; place-items:center; margin-bottom:14px;
font-family:var(--f-head); font-weight:900; font-size:.86rem; background:var(--gold-2); color:var(--ink);
}
.pillar h4{ color:#fff; font-size:1rem; margin-bottom:6px; }
.pillar p{ font-size:.85rem; color:#A9BBD4; line-height:1.8; }
.band{ margin-top:64px; }
.band-h{ display:flex; align-items:center; gap:14px; margin-bottom:24px; }
.band-i{
width:46px; height:46px; border-radius:13px; flex-shrink:0; display:grid; place-items:center;
font-size:1.25rem; background:linear-gradient(135deg,var(--gold-2),var(--gold)); color:var(--ink);
box-shadow:0 8px 22px rgba(201,162,39,.28);
}
.band.portal .band-i{ background:linear-gradient(135deg,#8CC0F5,#3E8CE0); color:#061A33; box-shadow:0 8px 22px rgba(62,140,224,.28); }
.band-h h3{ color:#fff; font-size:1.4rem; line-height:1.3; }
.band-h small{ display:block; font-family:var(--f-body); font-weight:500; font-size:.82rem; color:#8FA6C4; }
/* The rule runs from the heading toward the count; the page is RTL, so the
gradient fades toward the left. */
.band-h::after{ content:''; order:2; flex:1; height:1px; background:linear-gradient(to left, rgba(201,162,39,.45), transparent); }
.band.portal .band-h::after{ background:linear-gradient(to left, rgba(140,192,245,.45), transparent); }
.band-n{
order:3; flex-shrink:0; font-family:var(--f-head); font-weight:800; font-size:.78rem; color:var(--gold-2);
border:1px solid rgba(201,162,39,.4); border-radius:99px; padding:5px 14px;
}
.band.portal .band-n{ color:#8CC0F5; border-color:rgba(140,192,245,.4); }
.fgrid{ columns:3; column-gap:18px; }
.fcard{
break-inside:avoid; margin-bottom:18px; padding:20px 20px 18px; border-radius:18px;
background:rgba(255,255,255,.035); border:1px solid rgba(255,255,255,.09);
transition:border-color .25s, background .25s, transform .25s;
}
.fcard:hover{ border-color:rgba(201,162,39,.42); background:rgba(255,255,255,.055); transform:translateY(-3px); }
.band.portal .fcard:hover{ border-color:rgba(140,192,245,.42); }
.fcard-h{ display:flex; align-items:center; gap:12px; margin-bottom:14px; }
.fcard-h i{
width:38px; height:38px; border-radius:11px; flex-shrink:0; display:grid; place-items:center;
font-style:normal; font-size:1.05rem; background:rgba(201,162,39,.14); color:var(--gold-2);
}
.band.portal .fcard-h i{ background:rgba(62,140,224,.16); color:#8CC0F5; }
.fcard-h i .ico, .band-i .ico{ width:1.15em; height:1.15em; }
.fcard-h h4{ color:#fff; font-size:1rem; }
.chips{ list-style:none; display:flex; flex-wrap:wrap; gap:7px; }
.chips li{
display:inline-flex; align-items:center; gap:8px; padding:5px 12px; border-radius:99px;
font-size:.8rem; line-height:1.55; color:#D5E0EF;
background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08);
}
.chips li::before{ content:''; width:5px; height:5px; border-radius:50%; flex-shrink:0; background:var(--gold-2); }
.band.portal .chips li::before{ background:#8CC0F5; }
html,body{ background:#fff; } @media (max-width:1020px){
section{ break-before:page; padding:46px 30px; } .feat-stats{ grid-template-columns:repeat(2,1fr); }
.hero{ break-before:auto; min-height:auto; padding:56px 30px 48px; } .feat-stats > div:nth-child(3){ border-inline-start:0; }
#problem{ break-before:auto; } .feat-stats > div:nth-child(n+3){ border-top:1px solid rgba(255,255,255,.08); }
.pillars{ grid-template-columns:repeat(2,1fr); }
.fgrid{ columns:2; }
}
@media (max-width:760px){
.pillars{ grid-template-columns:1fr; }
.fgrid{ columns:1; }
.band-h::after{ display:none; }
}
/* Never split an atomic block across a page boundary. */ /* ================= PRINT / PDF (A4) =================
.card,.pain,.tmpl,.pay-step,.tl-i,.faq,.kpi,.pline,.note,.drop, Both the browser's own Print and generate-pdf.mjs land here. The screen
.price-hero,.phone,.mcard,.stat,.fstep,.tbl,.scope-list li,.shot-grid button,figure{ break-inside:avoid; } page is built for scrolling: reveals start invisible, the nav floats, the
viewers show one screenshot at a time, and dark bands carry the design.
Paper needs the opposite — everything visible, printer-safe margins,
ink-light pages, and every screenshot laid out as a gallery. Sized for A4
at 100% scale, so "actual size" prints exactly what the PDF shows. */
.print-app, .print-portal{ display:none; }
.faq{ break-inside:avoid; } @page{ size:A4; margin:14mm 12mm 16mm; }
.faq p{ display:block !important; } /* <details> print collapsed otherwise */
footer{ break-before:page; }
/* Watermarks read as grey smudges on paper at screen opacity. */ @media print{
.wm::before,.hero::before,.price-hero::before,footer::before{ opacity:.045 !important; } html{ font-size:12px; }
body{ font-size:12px; line-height:1.7; background:#fff; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
a{ text-decoration:none; color:inherit; }
.nav,.progress,.btn-row,.screen-only{ display:none !important; }
.reveal{ opacity:1 !important; transform:none !important; transition:none !important; }
.wrap,.narrow{ max-width:none; }
/* The page margin replaces the screen padding. */
section{ padding:0 !important; break-before:page; }
section.alt, section.dark{ background:#fff !important; color:var(--txt); }
.wm::before, .feat::after, .price-hero::before, footer::before{ display:none !important; }
h2.title{ font-size:1.95rem; margin-bottom:3mm; }
.lede{ font-size:1rem; max-width:none; }
.eyebrow{ margin-bottom:2mm; }
h2,h3,h4,.eyebrow{ break-after:avoid; }
/* Cover: the hero fills the first page. 265mm, not the full 267mm content
box, so rounding never pushes it onto a second page. */
.hero{ break-before:auto; min-height:0; height:265mm; border-radius:4mm; padding:14mm 12mm !important; }
.hero-logo{ height:32mm; margin-bottom:7mm; }
.hero h1{ font-size:3rem; }
.hero-sub{ font-size:1.08rem; margin-bottom:9mm; }
.hero-stats{ margin-bottom:9mm; }
.hero-stat{ padding:0 6mm; }
.hero-stat b{ font-size:1.6rem; }
/* Grids keep their desktop columns — at A4 width the phone breakpoints
would otherwise stack everything into one long column. */
.g2{ grid-template-columns:repeat(2,1fr) !important; }
.g3{ grid-template-columns:repeat(3,1fr) !important; }
.g4{ grid-template-columns:repeat(4,1fr) !important; }
.grid{ gap:4mm !important; }
.card{ padding:4.5mm; }
.card:hover,.tmpl:hover,.fcard:hover{ transform:none; box-shadow:none; }
.pain{ padding:4mm; }
.note{ padding:4mm 4.5mm; }
/* Dark bands print light: they are ink-heavy, and white text disappears on
a monochrome printer. */
.dark h2,.dark h3,.dark h4,.dark h2.title{ color:var(--navy) !important; }
.dark .lede{ color:var(--txt-2) !important; }
.dark .eyebrow{ color:#9A7B12 !important; }
.dark .card{ background:#fff !important; border-color:var(--line) !important; }
.dark .card p{ color:var(--txt-2) !important; }
.dark .stat{ background:var(--paper-2) !important; border-color:var(--line) !important; }
.dark .stat b{ color:var(--navy) !important; }
.dark .stat span{ color:var(--txt-3) !important; }
.flow{ grid-template-columns:repeat(4,1fr) !important; row-gap:0 !important; }
.fstep::after{ display:none; }
.fstep .fn{ background:var(--paper-2); color:#9A7B12; }
.fstep p{ color:var(--txt-2); }
/* Features */
.feat-stats{ grid-template-columns:repeat(4,1fr) !important; margin-top:6mm; background:var(--paper-2); border-color:var(--line); }
.feat-stats > div{ padding:4.5mm 3mm; }
.feat-stats > div + div{ border-inline-start:.3mm solid var(--line) !important; border-top:0 !important; }
.feat-stats b{ color:var(--navy); font-size:2.2rem; }
.feat-stats span{ color:var(--txt-2); }
.pillars{ grid-template-columns:repeat(2,1fr) !important; gap:4mm; margin-top:4mm; }
.pillar{ background:var(--paper-2); border-color:var(--line); padding:4.5mm; }
.pillar h4{ color:var(--navy); }
.pillar p{ color:var(--txt-2); }
.band{ margin-top:8mm; }
.band-h{ margin-bottom:4mm; }
.band-i{ box-shadow:none; }
.band-h h3{ color:var(--navy); }
.band-h small{ color:var(--txt-3); }
.band-h::after{ display:block !important; background:var(--line) !important; }
.band-n{ color:#9A7B12; border-color:rgba(201,162,39,.5); }
.band.portal .band-n{ color:#1F5FA8; border-color:rgba(62,140,224,.45); }
/* Multi-column layout fragments out of order across printed pages, so paper
gets a plain two-column grid instead of the screen's masonry. */
.fgrid{ columns:auto !important; display:grid; grid-template-columns:repeat(2,1fr); gap:4mm; align-items:start; }
.fcard{ background:#fff; border-color:var(--line); padding:4mm 4.5mm; margin-bottom:0; }
.fcard-h{ margin-bottom:3mm; }
.fcard-h h4{ color:var(--navy); }
.fcard-h i{ background:var(--gold-dim); color:#9A7B12; }
.band.portal .fcard-h i{ background:rgba(62,140,224,.12); color:#1F5FA8; }
.chips li{ color:var(--txt); background:var(--paper-2); border-color:var(--line); padding:1mm 3mm; font-size:.82rem; }
.chips li::before{ background:var(--gold); }
.band.portal .chips li::before{ background:var(--sky); }
/* Screenshot galleries (built by js/app.js, hidden on screen). */
.print-app{ display:grid; grid-template-columns:repeat(5,1fr); gap:5mm 3.5mm; margin-top:6mm; }
.print-app figure{ margin:0; text-align:center; }
.print-app img{ display:block; width:100%; height:auto; border-radius:3mm; border:.3mm solid var(--line-2); }
.print-app figcaption{ margin-top:1.8mm; font-family:var(--f-head); font-weight:700; font-size:.82rem; line-height:1.35; color:var(--navy); }
.print-portal{ display:grid; grid-template-columns:repeat(2,1fr); gap:6mm 5mm; margin:6mm 0; }
.print-portal figure{ margin:0; }
.print-portal figure.lead{ grid-column:1 / -1; }
.print-portal img{ display:block; width:100%; height:auto; border-radius:2mm; border:.3mm solid var(--line-2); }
.print-portal figcaption{ margin-top:2mm; }
.print-portal figcaption b{ display:block; font-family:var(--f-head); font-size:.92rem; color:var(--navy); }
.print-portal figcaption span{ display:block; font-size:.8rem; line-height:1.6; color:var(--txt-2); }
/* Pricing */
.price-hero{ padding:8mm 9mm; border-radius:4mm; }
.price-hero .amt{ font-size:3.3rem; }
.pay-step{ padding:4mm; }
/* The templates and the work cycle share a page when they fit, and so do
the work cycle and the client inputs. */
#flow{ break-before:auto; break-inside:avoid; margin-top:10mm; }
#inputs{ break-before:auto; break-inside:avoid; margin-top:10mm; }
/* The portal band starts its own page; otherwise its heading is stranded
at the foot of the app band's last page. */
#band-portal{ break-before:page; margin-top:0; }
.band-h{ break-after:avoid; }
/* Pricing and the terms each close on one page instead of spilling a
single block onto an otherwise empty sheet. */
#price .grid{ margin-top:5mm !important; }
/* The terms read as the tail of the offer, so they follow the pricing
cards on the same sheet rather than leaving it two-thirds empty. */
#terms{ break-before:auto; margin-top:12mm; }
#price .pline{ padding:2.2mm 0; }
#price .scope-list li{ padding:1.4mm 0; }
.faq p{ font-size:.94rem; line-height:1.72; }
.faq summary{ padding:3mm 0 1.5mm; }
.faq summary::after{ display:none; }
.faq p{ display:block !important; padding:0 0 2.4mm; }
footer{ break-before:avoid; break-inside:avoid; background:#fff !important; color:var(--txt-2); border-top:.3mm solid var(--line); padding:8mm 0 0 !important; margin-top:10mm; }
footer img{ content:url(../assets/al-arcade-dark.png); height:11mm; }
footer > div{ color:var(--navy) !important; }
footer .fl span{ color:var(--txt); }
footer small{ color:var(--txt-3); }
a{ text-decoration:none; } /* Never split an atomic block across a page boundary. */
.card,.pain,.tmpl,.pay-step,.faq,.pline,.note,.price-hero,.stat,.fstep,.fcard,.pillar,
.feat-stats,.scope-list li,.print-app figure,.print-portal figure{ break-inside:avoid; }
} }
/** /**
* Renders the proposal microsite to PDF. * Renders the proposal microsite to an A4 PDF.
* *
* The site is built for scrolling: the phone shows one app screenshot at a * All the print layout lives in css/styles.css under @media print, and the
* time and the browser frame one portal screenshot at a time. On paper that * screenshot galleries are built into the page by js/app.js — so this script
* would hide almost every screen, so both viewers are replaced by labelled * and the browser's own Print produce the same document. All it adds is
* grids of all their screenshots before printing. * page numbers in the footer.
* *
* node generate-pdf.mjs # from the working tree * node generate-pdf.mjs # from the working tree
* node generate-pdf.mjs --live # from the deployed site * node generate-pdf.mjs --live # from the deployed site
...@@ -20,85 +20,45 @@ const SRC = LIVE ...@@ -20,85 +20,45 @@ const SRC = LIVE
: 'file://' + path.join(__dirname, 'index.html'); : 'file://' + path.join(__dirname, 'index.html');
const OUT = path.join(__dirname, 'عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf'); const OUT = path.join(__dirname, 'عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf');
const wait = ms => new Promise(r => setTimeout(r, ms)); // Kept equal to the @page margins in css/styles.css.
const MARGIN = { top: '14mm', bottom: '16mm', left: '12mm', right: '12mm' };
const FOOTER = `
<div style="width:100%; padding:0 12mm; font-size:7.5px; color:#7C8AA0; direction:rtl;
display:flex; justify-content:space-between; font-family:'Geeza Pro','Arial',sans-serif;">
<span>نادي الصيد المصري — تطبيق الأعضاء وبوابة الإدارة · عرض فني ومالي</span>
<span dir="ltr"><span class="pageNumber"></span> / <span class="totalPages"></span></span>
</div>`;
(async () => { (async () => {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless: 'new', headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--allow-file-access-from-files', '--font-render-hinting=none'], args: ['--no-sandbox', '--disable-setuid-sandbox', '--allow-file-access-from-files', '--font-render-hinting=none'],
}); });
const page = await browser.newPage(); const page = await browser.newPage();
await page.setViewport({ width: 1240, height: 1754, deviceScaleFactor: 2 }); await page.setViewport({ width: 1240, height: 1754, deviceScaleFactor: 2 });
console.log('· loading', LIVE ? 'live site' : 'working tree'); console.log('· loading', LIVE ? 'live site' : 'working tree');
await page.goto(SRC, { waitUntil: 'networkidle0', timeout: 90000 }); await page.goto(SRC, { waitUntil: 'networkidle0', timeout: 90000 });
await page.emulateMediaType('print'); await page.emulateMediaType('print');
await page.evaluate(() => window.dispatchEvent(new Event('beforeprint')));
console.log('· expanding screenshot viewers'); console.log('· waiting for gallery images and fonts');
await page.evaluate(() => { await page.waitForFunction(() => {
document.querySelectorAll('.reveal').forEach(el => el.classList.add('in')); const imgs = [...document.querySelectorAll('.print-app img, .print-portal img')];
const nav = document.getElementById('nav'); if (nav) nav.remove(); return imgs.length > 0 && imgs.every(i => i.complete && i.naturalWidth > 0);
}, { timeout: 90000 });
/* --- app: the single-phone viewer goes; the all-screens grid stays and
becomes the printed gallery, with its images loaded eagerly. --- */
const proto = document.querySelector('#app .proto');
if (proto) proto.remove();
document.querySelectorAll('#shot-grid img').forEach(img => { img.loading = 'eager'; });
/* --- portal: button row + single frame -> every screen, captioned --- */
const btns = [...document.querySelectorAll('.pn-item[data-pshot]')];
const shots = btns.map(b => ({ k: b.dataset.pshot, t: b.textContent.replace(/^\s*\S+\s+/, '').trim() }));
const row = btns.length ? btns[0].parentElement : null;
const frame = document.querySelector('#portal .browser');
const note = document.getElementById('pshot-note');
if (frame) {
frame.insertAdjacentHTML('beforebegin', `
<div class="print-portal">
${shots.map(s => `
<figure>
<img src="screenshots/portal/${s.k}.jpg" alt="">
<figcaption>${s.t}</figcaption>
</figure>`).join('')}
</div>`);
frame.remove();
}
if (row) row.remove();
if (note) note.remove();
document.querySelectorAll('details').forEach(d => d.open = true);
const css = document.createElement('style');
css.textContent = `
/* Six across keeps all seventeen screens on one page; at five the last
row spilled onto a page of its own. */
.shot-grid{ grid-template-columns:repeat(6,1fr); gap:14px 12px; }
.shot-grid button{ border:0; padding:0; box-shadow:none; }
.shot-grid img{ border:1px solid var(--line); }
.print-portal{ display:grid; grid-template-columns:repeat(2,1fr); gap:22px 18px; margin-top:26px; }
.print-portal figure{ margin:0; break-inside:avoid; }
.print-portal img{ display:block; width:100%; height:auto; border:1px solid var(--line); border-radius:10px; }
.print-portal figcaption{ margin-top:7px; font-family:var(--f-head); font-weight:700; font-size:.82rem; color:var(--navy); }
`;
document.head.appendChild(css);
});
console.log('· waiting for images');
await page.waitForFunction(
() => [...document.images].every(img => img.complete && img.naturalWidth > 0),
{ timeout: 60000 },
);
await page.evaluate(() => document.fonts.ready); await page.evaluate(() => document.fonts.ready);
await wait(1200);
console.log('· printing'); console.log('· printing A4');
await page.pdf({ await page.pdf({
path: OUT, path: OUT,
width: '1240px', format: 'A4',
height: '1754px',
printBackground: true, printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 }, margin: MARGIN,
displayHeaderFooter: false, displayHeaderFooter: true,
headerTemplate: '<span></span>',
footerTemplate: FOOTER,
}); });
await browser.close(); await browser.close();
......
...@@ -157,97 +157,133 @@ ...@@ -157,97 +157,133 @@
</div> </div>
</section> </section>
<!-- ============ SCOPE ============ --> <!-- ============ FEATURES ============ -->
<section class="alt wm" id="scope"> <section class="dark feat" id="scope">
<div class="wrap"> <div class="wrap">
<div class="reveal"> <div class="reveal feat-head">
<div class="eyebrow">حدود المشروع</div> <div class="eyebrow">ما يشمله النظام</div>
<h2 class="title">ما يقدّمه النظام: أربعة محاور</h2> <h2 class="title">كل ما يحتاجه النادي، في نظام واحد</h2>
<p class="lede"> <p class="lede">
حصر النطاق في أربعة محاور هو ما يجعل التنفيذ ممكنًا في أسبوعين إلى أربعة أسابيع تطبيق للعضو على هاتفه، وبوابة لإدارة النادي على الويب، وتطبيق لماسح البوابة &mdash;
وبميزانية 500,000 جنيه غير شاملة ضريبة القيمة المضافة. ثلاث واجهات تعمل على قاعدة بيانات واحدة. وكل ما يلي مبنيّ ويعمل اليوم.
وما يقع خارج هذه المحاور غير مشمول في هذا العرض، ويُسعَّر على حدة إن طلبه النادي.
</p>
</div>
<div class="grid g4 reveal" style="margin-top:36px;">
<div class="card">
<div class="card-ico"><svg class="ico"><use href="#i-members"/></svg></div>
<h4>١ &mdash; العضوية والاشتراكات</h4>
<p>
تجديد العضوية السنوي وأقساطه. يرى العضو مستحقاته شهرًا بشهر،
ويسدد إلكترونيًا كاملًا أو قسطًا، ويستلم إيصالًا فوريًا في التطبيق.
</p>
</div>
<div class="card">
<div class="card-ico mint"><svg class="ico"><use href="#i-activity"/></svg></div>
<h4>٢ &mdash; الأنشطة الرياضية</h4>
<p>
كتالوج معلن بأسعار كل نشاط، واشتراك مباشر من التطبيق، وفاتورة شهرية،
وجدول تدريب يظهر لكل مشترك.
</p>
</div>
<div class="card">
<div class="card-ico sky"><svg class="ico"><use href="#i-gate"/></svg></div>
<h4>٣ &mdash; الدخول والدعوات</h4>
<p>
كود دخول شخصي متجدد لكل عضو، ورصيد دعوات شهري حسب نوع العضوية،
وشراء دعوات إضافية من التطبيق عند نفادها.
</p> </p>
</div> </div>
<div class="card">
<div class="card-ico rose"><svg class="ico"><use href="#i-news"/></svg></div>
<h4>٤ &mdash; أخبار النادي</h4>
<p>
قسم أخبار داخل التطبيق تنشر عليه الإدارة الأخبار والفعاليات والبطولات
بالصور، ويصل الإشعار للعضو فور النشر.
</p>
</div>
</div>
<div class="grid g2 reveal" style="margin-top:44px; align-items:start;"> <div class="feat-stats reveal">
<div> <div><b class="num">51</b><span>شاشة في تطبيق العضو</span></div>
<h3 style="color:var(--navy); font-size:1.1rem; margin-bottom:12px;">داخل النطاق</h3> <div><b class="num">34</b><span>قسمًا في بوابة الإدارة</span></div>
<ul class="scope-list"> <div><b class="num">33</b><span>معالجًا مُرشدًا للعمليات الجماعية</span></div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>تطبيق موبايل للأعضاء على iPhone و Android بواجهة عربية</span></li> <div><b class="num">6</b><span>أدوار بصلاحيات منفصلة</span></div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>دخول العضو برقم موبايله بكود تحقق &mdash; بلا كلمات مرور</span></li> </div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>بطاقة عضوية رقمية توضح سريان الاشتراك</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>كشف المستحقات: تجديد سنوي وأقساط وغرامات وفواتير أنشطة</span></li> <div class="pillars reveal">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>سداد إلكتروني بالبطاقة والمحفظة مع إيصال فوري</span></li> <div class="pillar">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>كتالوج الأنشطة بالأسعار والفئات السنية والأماكن المتاحة</span></li> <div class="pillar-n">١</div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>الاشتراك في نشاط وربطه بأحد الأبناء</span></li> <h4>العضوية والاشتراكات</h4>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>جدول التدريب الأسبوعي لكل مشترك</span></li> <p>تجديد العضوية السنوي وأقساطه. يرى العضو مستحقاته شهرًا بشهر، ويسدد إلكترونيًا كاملًا أو قسطًا، ويستلم إيصالًا فوريًا في التطبيق.</p>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>كود دخول متجدد وشاشة مسح على بوابات النادي</span></li> </div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>رصيد الدعوات وإصدارها وشراء دعوات إضافية</span></li> <div class="pillar">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>قسم أخبار النادي بالصور داخل التطبيق</span></li> <div class="pillar-n">٢</div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>إشعارات بالمستحقات وبالأخبار الجديدة فور نشرها</span></li> <h4>الأنشطة الرياضية</h4>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>أقساط بجدول محدَّد وسعر معلن للسداد المبكر</span></li> <p>كتالوج معلن بأسعار كل نشاط، واشتراك مباشر من التطبيق، وفاتورة شهرية، وجدول تدريب يظهر لكل مشترك.</p>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>غرامات بأسبابها، وتظلّم من التطبيق يردّ عليه موظف الإدارة</span></li> </div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>رصيد دائن يُخصم تلقائيًا من الفاتورة التالية</span></li> <div class="pillar">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>رفع إيصال التحويل البنكي من التطبيق واعتماده من البوابة</span></li> <div class="pillar-n">٣</div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>شهادة طبية تُرفع بالتصوير وتُراجع وتُعتمد أو تُرفض بسبب مكتوب</span></li> <h4>الدخول والدعوات</h4>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>حضور اللاعبين وتقييمهم، ونشر التقييم لولي الأمر</span></li> <p>كود دخول شخصي متجدد لكل عضو، ورصيد دعوات شهري حسب نوع العضوية، وشراء دعوات إضافية من التطبيق عند نفادها.</p>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>فعاليات النادي بفئات تذاكر وسياسة استرداد وحجز من التطبيق</span></li> </div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>بوابة ويب لإدارة الأعضاء والمستحقات والتسعير والدعوات والأخبار</span></li> <div class="pillar">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>سجل مراجعة يوضّح من غيّر أي حقل ومن أي قيمة إلى أي قيمة</span></li> <div class="pillar-n">٤</div>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>نشر التطبيق على App Store و Google Play باسم النادي</span></li> <h4>أخبار النادي</h4>
</ul> <p>قسم أخبار داخل التطبيق تنشر عليه الإدارة الأخبار والفعاليات والبطولات بالصور، ويصل الإشعار للعضو فور النشر.</p>
</div> </div>
<div> </div>
<h3 style="color:var(--txt-3); font-size:1.1rem; margin-bottom:12px;">خارج النطاق</h3>
<ul class="scope-list"> <div class="band reveal" id="band-app">
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>حجز الملاعب والكورتات وحمامات السباحة</span></li> <div class="band-h">
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>محفظة إلكترونية ورصيد داخلي</span></li> <div class="band-i"><svg class="ico"><use href="#i-wallet"/></svg></div>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>المطاعم والكافيتريات والطلب من التطبيق</span></li> <div><h3>تطبيق العضو</h3><small>iPhone و Android &mdash; بواجهة عربية بالكامل</small></div>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>القرى والشاليهات والحجوزات الموسمية</span></li> <span class="band-n"><span class="num" data-count-of="band-app"></span> ميزة</span>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>الشكاوى والطلبات والدعم الفني</span></li> </div>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>العيادة الطبية والكشف الرياضي داخل النادي</span></li> <div class="fgrid">
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>نظام ERP محاسبي كامل (دفتر عام وميزانيات وأصول)</span></li> <article class="fcard">
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>الموارد البشرية والرواتب والحضور</span></li> <div class="fcard-h"><i><svg class="ico"><use href="#i-shield"/></svg></i><h4>الدخول والأمان</h4></div>
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>المخازن والمشتريات وأذون الاستلام</span></li> <ul class="chips"><li>الدخول برقم الموبايل وكود تحقق</li><li>بلا كلمة مرور ولا تسجيل ذاتي</li><li>جهاز نشط واحد لكل عضو</li><li>قفل التطبيق ببصمة الإصبع</li><li>تغيير كود الدخول وإبطال القديم فورًا</li><li>جولة تعريفية عند أول استخدام</li></ul>
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>نقاط البيع داخل النادي</span></li> </article>
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>إدارة الموظفين والصلاحيات المعقدة</span></li> <article class="fcard">
</ul> <div class="fcard-h"><i><svg class="ico"><use href="#i-members"/></svg></i><h4>العضوية والأسرة</h4></div>
<ul class="chips"><li>بطاقة عضوية رقمية بملء الشاشة</li><li>التبديل بين أفراد الأسرة بلمسة</li><li>صلاحيات ولي الأمر على كل فرد</li><li>تنبيه على صورة من عليه مستحقات</li><li>الملف الشخصي وتغيير الصورة</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-card"/></svg></i><h4>المستحقات والسداد</h4></div>
<ul class="chips"><li>كشف موحّد: مستحق الآن وقادم ومتأخر</li><li>توزيع التأخير بعدد الأيام</li><li>سداد عدة بنود في عملية واحدة</li><li>بطاقة بنكية ومحفظة إلكترونية</li><li>رصيد دائن يُخصم تلقائيًا</li><li>إيصال PDF فوري قابل للمشاركة</li><li>سجل المدفوعات وأرشيف الإيصالات</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-receipt"/></svg></i><h4>الأقساط والغرامات</h4></div>
<ul class="chips"><li>خطط تقسيط بجدول كل قسط وحالته</li><li>سعر معلن للسداد المبكر</li><li>غرامات بأسبابها</li><li>تظلّم من التطبيق وردّ النادي عليه</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-gate"/></svg></i><h4>الدخول والدعوات</h4></div>
<ul class="chips"><li>كود QR يتجدد كل ثلاثين ثانية</li><li>يعمل دون اتصال بالإنترنت</li><li>كود مستقل لكل فرد في الأسرة</li><li>سجل الدخول إلى النادي</li><li>رصيد الدعوات الشهري</li><li>إصدار دعوة ومشاركتها مع الضيف</li><li>شراء دعوات إضافية</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-activity"/></svg></i><h4>الأنشطة الرياضية</h4></div>
<ul class="chips"><li>كتالوج مُصفّى بسنّ الفرد</li><li>اشتراك فوري أو قائمة انتظار</li><li>رسوم محسوبة بالتناسب قبل التأكيد</li><li>جدول الأسبوع للأسرة كلها</li><li>نسبة الحضور وتقييمات المدرب</li><li>إبلاغ عن غياب أو إيقاف مؤقت أو إلغاء</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-file"/></svg></i><h4>الشهادات الطبية</h4></div>
<ul class="chips"><li>رفع بالتصوير أو من المعرض</li><li>أكثر من صفحة في الشهادة الواحدة</li><li>ترفيهي وأكاديمي ودولي</li><li>متابعة حالة المراجعة وسببها</li><li>تنبيه قبل انتهاء الشهادة</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-news"/></svg></i><h4>حياة النادي</h4></div>
<ul class="chips"><li>الأخبار والإعلانات بالصور</li><li>رحلات وبطولات وحج وعمرة وفعاليات اجتماعية</li><li>حجز للأسرة والضيوف</li><li>رفع مستندات الحجز</li><li>تذكرة بكود لكل مشترك</li><li>مبلغ الاسترداد معروض قبل الإلغاء</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-bell"/></svg></i><h4>الإشعارات والإعدادات</h4></div>
<ul class="chips"><li>مركز الإشعارات</li><li>تفضيلات لكل فئة: التطبيق والرسائل والبريد</li><li>مظهر فاتح وداكن وتلقائي</li><li>أرقام عربية أو إنجليزية</li><li>آخر بيانات محفوظة عند انقطاع الشبكة</li><li>الأسئلة الشائعة والتواصل مع النادي</li></ul>
</article>
</div>
</div>
<div class="band portal reveal" id="band-portal">
<div class="band-h">
<div class="band-i"><svg class="ico"><use href="#i-dashboard"/></svg></div>
<div><h3>بوابة الإدارة وتطبيق البوابة</h3><small>على الويب &mdash; يرى كل موظف ما يخصّ دوره</small></div>
<span class="band-n"><span class="num" data-count-of="band-portal"></span> ميزة</span>
</div>
<div class="fgrid">
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-dashboard"/></svg></i><h4>التحصيل والمال</h4></div>
<ul class="chips"><li>لوحة التحصيل وتنبيهات ذكية</li><li>تفاصيل الإيرادات</li><li>مطابقة التحصيل مع بوابة الدفع</li><li>المتأخرات وإرسال التذكير</li><li>دورات الفوترة بتشغيل تجريبي</li><li>المرتجعات والاسترداد</li><li>إثباتات التحويل البنكي</li><li>طلبات التقسيط والتظلمات</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-members"/></svg></i><h4>الأعضاء والعضويات</h4></div>
<ul class="chips"><li>ملف العضو بسبعة عشر قسمًا</li><li>مستحق يدوي وسداد نقدي أو بشيك</li><li>إعفاء بسبب مسجَّل</li><li>خطة تقسيط وغرامة من الملف</li><li>إنهاء جلسات الدخول عند فقد الهاتف</li><li>دمج المكرر وتوحيد أرقام الموبايل</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-activity"/></svg></i><h4>الأنشطة</h4></div>
<ul class="chips"><li>البرامج والأسعار</li><li>المجموعات والمواعيد والسعة</li><li>الاشتراكات وقائمة الانتظار وطلبات النقل</li><li>تسجيل الحضور بالحالة الطبية لكل لاعب</li><li>تقييم اللاعبين ونشره لولي الأمر</li><li>تقرير الأنشطة وترحيل الموسم</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-shield"/></svg></i><h4>الطبي</h4></div>
<ul class="chips"><li>مراجعة الشهادات: اعتماد كامل أو مشروط أو رفض</li><li>شهادات على وشك الانتهاء</li><li>تقرير الالتزام الطبي</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-news"/></svg></i><h4>حياة النادي</h4></div>
<ul class="chips"><li>الأخبار مع التثبيت على الرئيسية</li><li>الفعاليات بفئات التذاكر وسياسة الاسترداد</li><li>مراجعة مستندات المشتركين</li><li>دعوة الأعضاء إلى الفعاليات</li><li>أداء الفعاليات</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-mail"/></svg></i><h4>البوابة والدعوات</h4></div>
<ul class="chips"><li>سجل الدخول ومن بداخل النادي الآن</li><li>أجهزة البوابات</li><li>سياسة الدعوات لكل نوع عضوية</li><li>منح دعوات استثنائية</li><li>قائمة الحظر</li><li>وضع الطوارئ</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-gate"/></svg></i><h4>تطبيق ماسح البوابة</h4></div>
<ul class="chips"><li>مسح كود العضو بكاميرا الجهاز</li><li>إدخال الكود يدويًا عند تعذّر المسح</li><li>يعمل عند انقطاع الشبكة ويرسل ما مسحه لاحقًا</li><li>تجاوز يدوي بكود الموظف</li></ul>
</article>
<article class="fcard">
<div class="fcard-h"><i><svg class="ico"><use href="#i-settings"/></svg></i><h4>العمليات والإدارة</h4></div>
<ul class="chips"><li><span class="num">33</span> معالجًا بمراجعة قبل التنفيذ</li><li>استيراد البيانات بستة قوالب وتراجع</li><li>حملات الإشعارات وقوالبها</li><li>الهوية والعلامة التجارية</li><li>المستخدمون والأدوار</li><li>سجل المراجعة لكل تغيير</li><li>تصدير بيانات النادي</li></ul>
</article>
</div> </div>
</div> </div>
</div> </div>
...@@ -265,7 +301,7 @@ ...@@ -265,7 +301,7 @@
</p> </p>
</div> </div>
<div class="proto reveal" style="margin-top:38px;"> <div class="proto reveal screen-only" style="margin-top:38px;">
<div class="proto-nav"> <div class="proto-nav">
<h4>شاشات التطبيق</h4> <h4>شاشات التطبيق</h4>
<button class="pn-item on" data-shot="01-signin"><span class="n">١</span> الدخول برقم الموبايل</button> <button class="pn-item on" data-shot="01-signin"><span class="n">١</span> الدخول برقم الموبايل</button>
...@@ -306,7 +342,7 @@ ...@@ -306,7 +342,7 @@
</div> </div>
</div> </div>
<div class="reveal" style="margin-top:44px;"> <div class="reveal screen-only" style="margin-top:44px;">
<h3 style="color:var(--navy);font-size:1.05rem;margin-bottom:14px;">كل الشاشات في لقطة واحدة</h3> <h3 style="color:var(--navy);font-size:1.05rem;margin-bottom:14px;">كل الشاشات في لقطة واحدة</h3>
<div class="shot-grid" id="shot-grid"></div> <div class="shot-grid" id="shot-grid"></div>
</div> </div>
...@@ -327,7 +363,7 @@ ...@@ -327,7 +363,7 @@
</p> </p>
</div> </div>
<div class="reveal" style="margin-top:34px; display:flex; gap:8px; flex-wrap:wrap;"> <div class="reveal screen-only" style="margin-top:34px; display:flex; gap:8px; flex-wrap:wrap;">
<button class="pn-item on" style="width:auto;" data-pshot="dashboard"><span class="n">١</span> لوحة التحصيل</button> <button class="pn-item on" style="width:auto;" data-pshot="dashboard"><span class="n">١</span> لوحة التحصيل</button>
<button class="pn-item" style="width:auto;" data-pshot="collections"><span class="n">٢</span> المتأخرات</button> <button class="pn-item" style="width:auto;" data-pshot="collections"><span class="n">٢</span> المتأخرات</button>
<button class="pn-item" style="width:auto;" data-pshot="members"><span class="n">٣</span> الأعضاء</button> <button class="pn-item" style="width:auto;" data-pshot="members"><span class="n">٣</span> الأعضاء</button>
...@@ -349,7 +385,7 @@ ...@@ -349,7 +385,7 @@
<button class="pn-item" style="width:auto;" data-pshot="audit"><span class="n">١٩</span> سجل المراجعة</button> <button class="pn-item" style="width:auto;" data-pshot="audit"><span class="n">١٩</span> سجل المراجعة</button>
</div> </div>
<div class="browser reveal" style="margin-top:18px;"> <div class="browser reveal screen-only" style="margin-top:18px;">
<div class="bbar"> <div class="bbar">
<div class="bdots"><i style="background:#FF5F57;"></i><i style="background:#FEBC2E;"></i><i style="background:#28C840;"></i></div> <div class="bdots"><i style="background:#FF5F57;"></i><i style="background:#FEBC2E;"></i><i style="background:#28C840;"></i></div>
<div class="burl" id="burl">https://portal.sayd-club.eg/portal</div> <div class="burl" id="burl">https://portal.sayd-club.eg/portal</div>
...@@ -358,7 +394,7 @@ ...@@ -358,7 +394,7 @@
alt="لوحة التحصيل في بوابة إدارة نادي الصيد المصري"> alt="لوحة التحصيل في بوابة إدارة نادي الصيد المصري">
</div> </div>
<div id="pshot-note" class="shot-note reveal"></div> <div id="pshot-note" class="shot-note reveal screen-only"></div>
<div class="note reveal" style="margin-top:24px;"> <div class="note reveal" style="margin-top:24px;">
<strong>بوابة بيانات لا نظام ERP:</strong> <strong>بوابة بيانات لا نظام ERP:</strong>
......
...@@ -212,4 +212,56 @@ ...@@ -212,4 +212,56 @@
} }
pBtns.forEach(function (b) { b.addEventListener('click', function () { showPortal(b.dataset.pshot); }); }); pBtns.forEach(function (b) { b.addEventListener('click', function () { showPortal(b.dataset.pshot); }); });
showPortal('dashboard'); showPortal('dashboard');
/* ---------- feature counts ----------
Counted from the lists themselves, so a band's number can never drift
from what the band actually shows. */
document.querySelectorAll('[data-count-of]').forEach(function (el) {
var band = document.getElementById(el.dataset.countOf);
if (band) el.textContent = band.querySelectorAll('.chips li').length;
});
/* ---------- print galleries ----------
On screen each viewer shows one screenshot at a time; paper needs all of
them. Both galleries live in the page (hidden on screen), so the browser's
own Print produces the same A4 document as generate-pdf.mjs. Their images
attach after load so they never compete with the first paint. */
var appWrap = document.querySelector('#app .wrap');
if (appWrap) {
var pa = document.createElement('div');
pa.className = 'print-app';
pa.innerHTML = shotBtns.map(function (b) {
var k = b.dataset.shot, n = SHOTS[k];
return n ? '<figure><img data-src="screenshots/app/' + k + '.jpg" alt="' + n.t + '">' +
'<figcaption>' + n.t + '</figcaption></figure>' : '';
}).join('');
appWrap.appendChild(pa);
}
var portalWrap = document.querySelector('#portal .wrap');
if (portalWrap) {
var pp = document.createElement('div');
pp.className = 'print-portal';
pp.innerHTML = pBtns.map(function (b, i) {
var k = b.dataset.pshot, n = PSHOTS[k];
return n ? '<figure' + (i === 0 ? ' class="lead"' : '') + '>' +
'<img data-src="screenshots/portal/' + k + '.jpg" alt="' + n.t + '">' +
'<figcaption><b>' + n.t + '</b><span>' + n.p + '</span></figcaption></figure>' : '';
}).join('');
portalWrap.insertBefore(pp, portalWrap.querySelector('.note'));
}
function loadPrintImages() {
document.querySelectorAll('.print-app img[data-src], .print-portal img[data-src]').forEach(function (img) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
}
if (document.readyState === 'complete') setTimeout(loadPrintImages, 300);
else window.addEventListener('load', function () { setTimeout(loadPrintImages, 300); });
// Closed <details> print as bare questions; open them all for paper.
window.addEventListener('beforeprint', function () {
loadPrintImages();
document.querySelectorAll('details').forEach(function (d) { d.open = true; });
});
})(); })();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment