
:root{ --card:#fff; --border:#e5e7eb; --text:#111827; --muted:#6b7280; --accent:#0a66c2; --ok:#0a7d44; --bad:#b91c1c; } *{box-sizing:border-box} body{margin:0;background:#f7f7f8;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;color:var(--text);padding:24px} .wrap{max-width:980px;margin:0 auto} .card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px} .calculator-card h1{font-size:22px;margin:0 0 8px 0} .muted{color:var(--muted)} .calculator-card .row{display:flex;gap:12px;flex-wrap:wrap} .col{flex:1 1 260px} label{font-size:12px;color:#374151;display:block;margin:6px 0} input[type="date"],input[type="number"]{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;font-size:14px;background:#fff} button{padding:10px 14px;border:1px solid var(--border);border-radius:10px;background:#f3f4f6;cursor:pointer;font-weight:600} button.primary{background:var(--accent);border-color:var(--accent);color:#fff} .stays{display:grid;grid-template-columns:1fr 1fr auto;gap:8px;align-items:end} .pill{display:inline-block;font-size:12px;padding:3px 8px;border-radius:999px;border:1px solid var(--border);background:#fff} .out{margin-top:14px;display:grid;gap:10px} .ok{color:var(--ok);font-weight:700} .bad{color:var(--bad);font-weight:700} .small{font-size:12px} .hr{height:1px;background:var(--border);margin:14px 0}
Investment Visa's Calculator
Official rolling rule: on any day of your stay, the last 180 days (including that day) must contain ≤ 90 days of presence across the whole Schengen Area. Entry/exit days count.
Note: does not constitute legal advice.
Past stays (add as many as needed) + Add stay
Planned entry date
Planned exit date (optional)
…or planned length (days)
Calculate Reset Window = last 180 days counting the day in question
/* ---------- Day helpers (UTC, day precision) ---------- */ const DAY = 86400000; const toDay = ymd => { if(!ymd) return null; const [y,m,d]=ymd.split('-').map(Number); return Math.floor(Date.UTC(y,(m||1)-1,(d||1))/DAY); }; const fromDay = n => new Date(n*DAY).toISOString().slice(0,10); const daysInclusive = (a,b)=>{ const out=[]; for(let x=a;x<=b;x++) out.push(x); return out; }; /* ---------- Build/merge past-stay ranges ---------- */ function normalizeStays(stays){ const ranges = stays .filter(s=>s.in && s.out) .map(s=>({a:Math.min(toDay(s.in),toDay(s.out)), b:Math.max(toDay(s.in),toDay(s.out))})) .sort((r1,r2)=>r1.a-r2.a); const merged=[]; for(const r of ranges){ if(!merged.length || r.a>merged[merged.length-1].b+1) merged.push({...r}); else merged[merged.length-1].b = Math.max(merged[merged.length-1].b, r.b); } return merged; } function buildPresenceSet(merged){ const S=new Set(); for(const {a,b} of merged) for(let d=a; d<=b; d++) S.add(d); return S; } function lastPresenceDay(merged){ if(!merged.length) return null; return merged[merged.length-1].b; } /* ---------- Rolling rule core ---------- */ function usedInWindow(pastSet, extraSet, day){ const a=day-179, b=day; let c=0; for(const d of pastSet) if(d>=a && d<=b) c++; if(extraSet) for(const d of extraSet) if(d>=a && d<=b) c++; return c; // entry/exit inclusive } function lastLegalExit(entryDay, pastSet){ const trip=new Set(); let lastOK=null; for(let d=entryDay; d<entryDay+366; d++){ trip.add(d); let ok=true; for(let x=entryDay; x<=d; x++){ if(usedInWindow(pastSet, trip, x) > 90){ ok=false; break; } } if(!ok){ trip.delete(d); break; } lastOK=d; } return lastOK; } function earliestEntryWithAnyDays(startFrom, pastSet){ for(let d=startFrom; d<startFrom+366; d++){ if(usedInWindow(pastSet,null,d) < 90) return d; } return null; } /* Fresh 90 at entry (depends ONLY on last past day): lastExit + 180 */ function earliestFresh90AtEntry(merged){ const L = lastPresenceDay(merged); if(L==null) return null; return L + 180; } /* ---------- UI ---------- */ const staysDiv=document.getElementById('stays'); const entryEl=document.getElementById('entry'); const exitEl=document.getElementById('exit'); const lenEl=document.getElementById('length'); const out=document.getElementById('out'); function addStayRow(a='',b=''){ const row=document.createElement('div'); row.style.display='contents'; row.innerHTML = ` <div><label class="small">Entry</label><input type="date" value="${a}" class="in"></div> <div><label class="small">Exit</label><input type="date" value="${b}" class="outd"></div> <div><button class="rm">Remove</button></div>`; row.querySelector('.rm').onclick=()=>row.remove(); staysDiv.appendChild(row); } document.getElementById('addStay').onclick=()=>addStayRow(); document.getElementById('reset').onclick=()=>{ staysDiv.innerHTML=''; out.innerHTML=''; addStayRow(); entryEl.value=new Date().toISOString().slice(0,10); exitEl.value=''; lenEl.value=''; }; /* keep exit in sync when length or entry changes; length wins */ function syncExitFromLength(){ const start=toDay(entryEl.value); const L=parseInt(lenEl.value,10); if(Number.isFinite(L) && L>0 && start!=null) exitEl.value = fromDay(start + (L-1)); } lenEl.addEventListener('input', ()=>{ if(lenEl.value) exitEl.value=''; syncExitFromLength(); }); entryEl.addEventListener('input', syncExitFromLength); exitEl.addEventListener('input', ()=>{ if(exitEl.value) lenEl.value=''; }); /* defaults */ addStayRow(); entryEl.value=new Date().toISOString().slice(0,10); /* ---------- Calculate ---------- */ document.getElementById('calc').onclick=()=>{ const rows=[...staysDiv.querySelectorAll('div[style*="contents"]')]; const stays=rows.map(r=>({in:r.querySelector('.in').value, out:r.querySelector('.outd').value})) .filter(s=>s.in && s.out); const merged=normalizeStays(stays); const pastSet=buildPresenceSet(merged); const entryStr=entryEl.value; if(!entryStr){ out.innerHTML=`<div class="bad">Please choose a planned entry date.</div>`; return; } const entryDay=toDay(entryStr); // derive exit from length if filled let plannedExitDay=null, computedNights=null; const L=parseInt(lenEl.value,10); if(Number.isFinite(L) && L>0){ plannedExitDay = entryDay + (L-1); } else if(exitEl.value){ plannedExitDay = toDay(exitEl.value); } // rolling stats for entryDay const winStart=entryDay-179, winEnd=entryDay; const usedAtEntry = usedInWindow(pastSet,null,entryDay); const leftAtEntry = Math.max(0, 90 - usedAtEntry); const lastExit = lastLegalExit(entryDay, pastSet); const earliestAny = earliestEntryWithAnyDays(entryDay, pastSet); // fresh-90-at-entry (based ONLY on previous stays) const fresh90 = earliestFresh90AtEntry(merged); // validate planned stay (rolling) let planOK=null, breach=null; if(plannedExitDay!=null){ computedNights = (plannedExitDay - entryDay + 1); const trip=new Set(daysInclusive(entryDay, plannedExitDay)); for(let d=entryDay; d<=plannedExitDay; d++){ if(usedInWindow(pastSet, trip, d) > 90){ planOK=false; breach=d; break; } } if(planOK===null) planOK=true; } // latest permitted exit messaging let latestExitDisplay = lastExit!==null ? fromDay(lastExit) : '—'; let extraNote = ''; if(usedAtEntry >= 90){ latestExitDisplay = '—'; if(earliestAny!==null){ const lastIfEnterThen = lastLegalExit(earliestAny, pastSet); extraNote = `<div class="small muted">Entry on ${entryStr} is not allowed. Earliest legal entry is ${fromDay(earliestAny)}; latest permitted exit if entering then is ${fromDay(lastIfEnterThen)}.</div>`; } } out.innerHTML = ` <div> <div><strong>180-day window at entry:</strong> ${fromDay(winStart)} → ${fromDay(winEnd)}</div> <div><strong>Days used in window:</strong> ${usedAtEntry}</div> <div><strong>Days available on ${entryStr}:</strong> ${leftAtEntry}</div> <div><strong>Latest permitted exit if entering on ${entryStr} (rolling rule):</strong> ${latestExitDisplay}</div> ${extraNote} <div class="small muted">Earliest date with any availability (rolling): ${earliestAny!==null ? fromDay(earliestAny) : '—'}</div> <div class="small muted"><strong>Earliest day to start a full 90-day stay</strong> (fresh 90 at entry, calculated as <em>last past day + 180</em>): ${fresh90!==null ? fromDay(fresh90) : '—'}</div> ${plannedExitDay!=null ? ` <div style="margin-top:8px;" class="${planOK?'ok':'bad'}"> ${planOK ? `Planned stay ${entryStr} → ${fromDay(plannedExitDay)} (${computedNights} days) is permitted.` : `Planned stay would breach the rule on ${fromDay(breach)}. Latest compliant exit for this entry date is ${fromDay(breach-1)}.`} </div> ` : `<div class="small muted" style="margin-top:8px;">Enter an exit date or a length in days to validate a specific trip.</div>`} </div> `; };