<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Nebula – Browser in your Browser</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%23aaf' d='M2 10a6 6 0 0 1 10.9-3A4 4 0 1 1 12 14H5.5A3.5 3.5 0 0 1 2 10z'/%3E%3C/svg%3E">
<style>
:root{
  --bg:#0b1220; --bg2:#0e1630; --panel:#111a32; --border:#1a2a54;
  --text:#e6eefc; --muted:#9fb0d4; --accent:#6fb1ff; --accent2:#93d0ff;
  --radius:12px; --radius-sm:9px; --shadow:0 10px 30px rgba(0,0,0,.28);
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:linear-gradient(180deg,var(--bg),#0a152c);color:var(--text);
  font:14px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
a{color:var(--accent)}
.app{height:100vh;display:grid;grid-template-rows:56px 44px 1fr}
.topbar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);
  background:linear-gradient(180deg,#0f1932,#0d1730)}
.brand{display:flex;align-items:center;gap:10px;font-weight:700}
.brand i{width:26px;height:26px;border-radius:8px;background:conic-gradient(from 210deg,#4e8cff,#17c964,#ffd055,#d66bff,#4e8cff);box-shadow:inset 0 0 0 2px rgba(255,255,255,.12),var(--shadow)}
.controls{display:flex;gap:8px;align-items:center}
.btn, .chip{
  background:#122147;border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:10px;display:inline-flex;gap:8px;align-items:center;cursor:pointer
}
.btn:hover{background:#162a57}
.btn:disabled{opacity:.5;cursor:not-allowed}
.urlbar{
  flex:1;display:flex;gap:8px;align-items:center;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:6px 8px
}
.urlbar input{flex:1;background:transparent;border:none;color:var(--text);outline:none;font:inherit}
.switch{display:inline-flex;align-items:center;gap:6px}
.switch input{appearance:none;width:36px;height:20px;background:#24406f;border:1px solid #33558b;border-radius:999px;position:relative;cursor:pointer}
.switch input:checked{background:#2d65ff}
.switch input::after{content:"";position:absolute;inset:2px auto 2px 2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:.18s}
.switch input:checked::after{left:calc(100% - 18px)}
.tabbar{display:flex;gap:8px;overflow:auto;padding:6px 8px;background:linear-gradient(180deg,#0e1830,#0b152c);border-bottom:1px solid var(--border)}
.tab{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--border);border-radius:999px;background:#0f1a33;cursor:pointer;white-space:nowrap;max-width:280px}
.tab.active{background:#13254d}
.tab .title{overflow:hidden;text-overflow:ellipsis}
.tab .close{opacity:.7}
.tab .close:hover{opacity:1}
.workspace{position:relative}
.webview{position:absolute;inset:0;border:0;background:#0a1326}
.toolbar-right{display:flex;gap:8px;align-items:center}
kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#0f1a33;border:1px solid #213a74;border-radius:6px;padding:2px 6px;font-size:12px;color:var(--muted)}
.dropdown{position:relative}
.menu{position:absolute;right:0;top:calc(100% + 6px);min-width:240px;background:var(--panel);border:1px solid var(--border);
  border-radius:12px;box-shadow:var(--shadow);padding:8px;display:none}
.menu.show{display:block}
.menu .row{display:flex;align-items:center;justify-content:space-between;padding:8px;border-radius:8px}
.menu .row:hover{background:#0f1d3e}
.badge{font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:999px;background:#122147;color:var(--muted)}
.notice{position:absolute;right:12px;bottom:12px;background:#0f1a33;border:1px solid var(--border);padding:10px;border-radius:10px;box-shadow:var(--shadow)}
.start{
  height:100%;display:grid;place-items:center;background:radial-gradient(1200px 600px at 50% -150px rgba(117,165,255,.22), transparent), #0a1428;
  color:#dfe9ff
}
.start .card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;box-shadow:var(--shadow);min-width:min(680px,92vw)}
.start h1{margin:4px 0 12px;font-size:22px}
.start .search{display:flex;gap:8px;align-items:center;background:#0e1934;border:1px solid var(--border);border-radius:12px;padding:10px}
.start input{flex:1;background:transparent;border:0;color:var(--text);font-size:16px;outline:none}
.start .row{display:flex;gap:12px;flex-wrap:wrap;color:var(--muted)}
hr{border:none;border-top:1px solid var(--border);margin:12px 0}
</style>
</head>
<body>
<div class="app" id="app">
  <!-- Top bar -->
  <div class="topbar">
    <div class="brand"><i></i> Nebula <span class="badge">in‑browser web browser</span></div>
    <div class="controls">
      <button class="btn" id="btnBack" title="Back (Alt+←)">⬅︎</button>
      <button class="btn" id="btnFwd" title="Forward (Alt+→)">➡︎</button>
      <button class="btn" id="btnReload" title="Reload (⌘/Ctrl+R)">⟳</button>
    </div>

    <div class="urlbar" id="urlbar">
      <span>🔗</span>
      <input id="address" placeholder="Type a URL or search…" spellcheck="false" autocomplete="off" />
      <button class="btn" id="btnGo" title="Go (Enter)">Go</button>
    </div>

    <div class="toolbar-right">
      <div class="switch" title="Allow popups in page">
        <label>Popups<input id="togglePopups" type="checkbox"></label>
      </div>
      <div class="switch" title="Treat iframe as same-origin (still cannot read cross-origin DOM)">
        <label>Same‑origin<input id="toggleSameOrigin" type="checkbox" checked></label>
      </div>
      <div class="switch" title="Allow camera/mic for embedded apps">
        <label>Cam/Mic<input id="toggleMedia" type="checkbox"></label>
      </div>
      <div class="dropdown">
        <button class="btn" id="btnMore">⋯</button>
        <div class="menu" id="menu">
          <div class="row"><span>New Tab <kbd>⌘/Ctrl+T</kbd></span><button class="btn" id="mNewTab">New</button></div>
          <div class="row"><span>Duplicate Tab</span><button class="btn" id="mDupTab">Duplicate</button></div>
          <div class="row"><span>New Start Page</span><button class="btn" id="mStart">Home</button></div>
          <div class="row"><span>Incognito (don’t save tabs)</span><input id="toggleIncog" type="checkbox"></div>
          <div class="row"><span>Clear Saved Tabs</span><button class="btn" id="mClear">Clear</button></div>
        </div>
      </div>
      <button class="btn" id="btnNewTab" title="New Tab (⌘/Ctrl+T)"></button>
    </div>
  </div>

  <!-- Tabs -->
  <div class="tabbar" id="tabbar"></div>

  <!-- Workspace -->
  <div class="workspace" id="workspace"></div>
</div>

<div class="notice" id="notice" style="display:none"></div>

<script>
/* ========= Helpers ========= */
const $ = s => document.querySelector(s);
const $$ = s => Array.from(document.querySelectorAll(s));
const isProbablyURL = v => /^[a-z]+:\/\//i.test(v) || v.includes('.') || v.startsWith('/');
function normalizeInput(input){
  input = input.trim();
  if (!input) return "about:blank";
  // Search if it doesn't look like a URL
  if (!/^[a-z]+:\/\//i.test(input) && !input.includes('.') && !input.startsWith('/')){
    const q = encodeURIComponent(input);
    return `https://duckduckgo.com/?q=${q}`;
  }
  // Add scheme if missing
  if (!/^[a-z]+:\/\//i.test(input)){
    return 'https://' + input.replace(/^\/+/,'');
  }
  return input;
}
const storageKey = 'nebula:tabs:v1';

/* ========= State ========= */
const App = {
  tabs: [],           // { id, title, url, iframe, icon, history:[], hIndex: number }
  activeId: null,
  persist: true
};

/* ========= Start Page ========= */
const START_HTML = (tip='') => `
<!doctype html>
<meta charset="utf-8">
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Nebula Start</title>
<style>
  :root{--border:#1a2a54;--panel:#0f1a33;--text:#e6eefc;--muted:#9fb0d4}
  body{margin:0;height:100vh;display:grid;place-items:center;background:#0a1428;color:var(--text);font:14px/1.4 system-ui,Segoe UI,Roboto}
  .card{min-width:min(720px,92vw);background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px}
  h1{margin:6px 0 10px;font-size:22px}
  .search{display:flex;gap:8px;align-items:center;background:#0e1934;border:1px solid var(--border);border-radius:12px;padding:12px}
  input{flex:1;border:0;background:transparent;color:var(--text);font-size:16px;outline:none}
  .row{display:flex;gap:10px;flex-wrap:wrap;color:var(--muted)}
  .chip{border:1px solid var(--border);border-radius:999px;padding:8px 10px;background:#0e1832}
  a{color:#93d0ff;text-decoration:none}
</style>
<div class=card>
  <h1>✨ Welcome to Nebula</h1>
  <div class=search>
    <span>🔎</span><input id=q placeholder="Search the web… (Enter)" autofocus />
  </div>
  <div class=row style="margin-top:12px">
    <span class=chip>Tip: ${tip}</span>
    <span class=chip>Open a URL like <b>https://example.com</b></span>
    <span class=chip>DuckDuckGo for search</span>
  </div>
</div>
<script>
  const $=s=>document.querySelector(s);
  addEventListener('keydown',e=>{
    if(e.key==='Enter'){ const v=$('#q').value.trim();
      let url=v;
      if(!/^([a-z]+:\\/\\/)/i.test(v)){
        if(!v.includes('.') && !v.includes(' ') && !v.startsWith('/')) url='https://'+v;
        else url='https://duckduckgo.com/?q='+encodeURIComponent(v);
      }
      location.href=url;
    }
  });
<\/script>
`;

/* ========= Tab Management ========= */
function saveTabs(){
  if (!App.persist) return;
  const serial = App.tabs.map(t => ({id:t.id,title:t.title,url:t.url, history:t.history, hIndex:t.hIndex||0}));
  localStorage.setItem(storageKey, JSON.stringify(serial));
}
function loadTabs(){
  const raw = localStorage.getItem(storageKey);
  if (!raw) return;
  try{
    const list = JSON.parse(raw);
    for(const t of list){ createTab(t.url || 'about:blank', t.title||'New Tab', t.history||[], t.hIndex||0); }
  }catch(e){ console.warn('restore failed', e); }
}
function createTab(url='about:blank', title='New Tab', history=[], hIndex=history.length-1){
  const id = 't'+Math.random().toString(36).slice(2,8);
  const tab = { id, title, url, iframe:null, icon:'🌐', history: history.length? history : [url], hIndex: hIndex };
  App.tabs.push(tab);
  mountTab(tab);
  setActive(id);
  return tab;
}
function closeTab(id){
  const idx = App.tabs.findIndex(t=>t.id===id);
  if (idx<0) return;
  const wasActive = App.activeId===id;
  const t = App.tabs[idx];
  t.iframe?.remove();
  App.tabs.splice(idx,1);
  renderTabbar();
  if (!App.tabs.length) createTab('data:text/html,'+encodeURIComponent(START_HTML('Use ⌘/Ctrl+L to focus URL bar')));
  if (wasActive) setActive(App.tabs[Math.max(0,idx-1)].id);
  saveTabs();
}
function duplicateTab(){
  const t = getActive(); if(!t) return;
  createTab(t.url, t.title, [...t.history], t.hIndex);
}
function renderTabbar(){
  const bar = $('#tabbar'); bar.innerHTML='';
  for(const t of App.tabs){
    const el = document.createElement('div');
    el.className = 'tab'+(t.id===App.activeId?' active':'');
    el.title = t.url;
    el.innerHTML = `<span>${t.icon}</span><span class=title>${t.title||'New Tab'}</span>
      <span class=close title="Close (⌘/Ctrl+W)">✕</span>`;
    el.querySelector('.close').onclick = (e)=>{ e.stopPropagation(); closeTab(t.id); };
    el.onclick = ()=> setActive(t.id);
    bar.appendChild(el);
  }
}
function setActive(id){
  App.activeId = id;
  for(const t of App.tabs){ t.iframe?.classList.add('hidden'); }
  const t = App.tabs.find(x=>x.id===id);
  if (!t) return;
  t.iframe?.classList.remove('hidden');
  $('#address').value = t.url || '';
  renderTabbar();
  updateNavButtons();
}
function getActive(){ return App.tabs.find(t=>t.id===App.activeId); }

/* ========= Iframe creation ========= */
function buildSandboxFlags(){
  const allowPopups = $('#togglePopups').checked;
  const sameOrigin = $('#toggleSameOrigin').checked;
  const media = $('#toggleMedia').checked;

  const allowTokens = [
    'allow-forms',
    'allow-pointer-lock',
    'allow-scripts',
    'allow-downloads',
    allowPopups ? 'allow-popups allow-popups-to-escape-sandbox' : '',
    sameOrigin ? 'allow-same-origin' : '',
    media ? 'allow-modals' : '',
  ].filter(Boolean).join(' ');
  const allowAttr = [
    'geolocation *',
    media ? 'camera *; microphone *' : '',
    'payment *; clipboard-read *; clipboard-write *',
    'fullscreen *',
  ].filter(Boolean).join('; ');

  return { sandbox: allowTokens, allow: allowAttr };
}
function mountTab(tab){
  const ws = $('#workspace');

  // Create iframe
  const frame = document.createElement('iframe');
  frame.className = 'webview';
  const { sandbox, allow } = buildSandboxFlags();
  frame.setAttribute('sandbox', sandbox);
  frame.setAttribute('allow', allow);

  // Set src (start page via data: URL if about:blank)
  const firstUrl = tab.url || 'about:blank';
  frame.src = firstUrl.startsWith('data:') || firstUrl==='about:blank'
    ? firstUrl
    : firstUrl;

  frame.addEventListener('load', ()=>{
    // Best-effort title update (cross-origin safe: we can't read the DOM unless same-origin,
    // but the browser will still give us a generic title we can keep; fallback to URL host)
    try{
      const doc = frame.contentDocument; // will throw if cross-origin
      if (doc && doc.title) tab.title = doc.title;
    }catch(e){
      // Cross-origin: derive a nicer label from URL
      try{
        const u = new URL(tab.url);
        tab.title = u.host;
        tab.icon = '🕸️';
      }catch{}
    }
    renderTabbar();
    updateNavButtons();
  });

  // Keep reference
  tab.iframe = frame;
  ws.appendChild(frame);

  // If it's a special start page, inject it
  if (tab.url === 'about:blank' || tab.url.startsWith('nebula:start')) {
    frame.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(START_HTML('Press ⌘/Ctrl+L to focus the URL bar'));
    tab.title = 'Start';
  }

  renderTabbar();
}

/* ========= Navigation ========= */
function navigateCurrent(to){
  const t = getActive(); if(!t) return;
  const url = normalizeInput(to);
  t.url = url;

  // Update sandbox flags dynamically by reconstructing iframe if toggles changed
  const { sandbox, allow } = buildSandboxFlags();
  const f = t.iframe;
  f.setAttribute('sandbox', sandbox);
  f.setAttribute('allow', allow);
  try{
    f.src = url;
  }catch(e){
    // Fallback to start page showing an error
    f.src = 'data:text/html,'+encodeURIComponent(`<h1>Navigation blocked</h1><p>${e.message}</p>`);
  }

  // History we track based on user-initiated navigations (we cannot observe in-page navigations cross-origin)
  if (t.history[t.hIndex] !== url){
    t.history = t.history.slice(0, t.hIndex+1);
    t.history.push(url);
    t.hIndex = t.history.length-1;
  }
  $('#address').value = url;
  t.title = url.replace(/^https?:\/\//,'');
  renderTabbar();
  updateNavButtons();
  saveTabs();
}
function back(){
  const t = getActive(); if(!t) return;
  if (t.hIndex>0){
    t.hIndex--; const url = t.history[t.hIndex];
    t.url = url; t.iframe.src = url; $('#address').value = url; updateNavButtons(); saveTabs();
  } else {
    // Try page history (cross-origin safe)
    try{ t.iframe.contentWindow.history.back(); }catch{}
  }
}
function forward(){
  const t = getActive(); if(!t) return;
  if (t.hIndex < t.history.length-1){
    t.hIndex++; const url = t.history[t.hIndex];
    t.url = url; t.iframe.src = url; $('#address').value = url; updateNavButtons(); saveTabs();
  } else {
    try{ t.iframe.contentWindow.history.forward(); }catch{}
  }
}
function reload(){
  const t = getActive(); if(!t) return;
  try{ t.iframe.contentWindow.location.reload(); }
  catch{ t.iframe.src = t.url; }
}
function updateNavButtons(){
  const t = getActive(); if(!t){ $('#btnBack').disabled = $('#btnFwd').disabled = $('#btnReload').disabled = true; return; }
  $('#btnBack').disabled = t.hIndex<=0;
  $('#btnFwd').disabled = t.hIndex>=t.history.length-1;
  $('#btnReload').disabled = !t.url;
}

/* ========= UI Wiring ========= */
$('#btnGo').onclick = ()=> navigateCurrent($('#address').value);
$('#address').addEventListener('keydown', e=>{
  if (e.key==='Enter'){ navigateCurrent(e.currentTarget.value); }
  if ((e.metaKey||e.ctrlKey) && e.key.toLowerCase()==='l'){ e.preventDefault(); e.currentTarget.select(); }
});
$('#btnBack').onclick = back;
$('#btnFwd').onclick = forward;
$('#btnReload').onclick = reload;

$('#btnMore').onclick = ()=>{
  $('#menu').classList.toggle('show');
};
addEventListener('click', (e)=>{
  if (!$('#btnMore').contains(e.target) && !$('#menu').contains(e.target)) $('#menu').classList.remove('show');
});

$('#mNewTab').onclick = ()=> createTab('about:blank');
$('#mDupTab').onclick = duplicateTab;
$('#mStart').onclick = ()=> { const t=getActive(); if(!t) return; t.url='about:blank'; t.iframe.src='data:text/html,'+encodeURIComponent(START_HTML('Welcome back!')); t.title='Start'; renderTabbar(); };
$('#mClear').onclick = ()=> { localStorage.removeItem(storageKey); showNotice('Saved tabs cleared.'); };

$('#toggleIncog').onchange = (e)=>{ App.persist = !e.currentTarget.checked; showNotice(App.persist?'Saving tabs is ON':'Incognito: tabs won’t be saved'); if (App.persist) saveTabs(); else localStorage.removeItem(storageKey); };
$('#togglePopups').onchange = $('#toggleSameOrigin').onchange = $('#toggleMedia').onchange = ()=>{
  // Re-apply sandbox/allow for current tab
  const t=getActive(); if(!t) return;
  const { sandbox, allow } = buildSandboxFlags();
  t.iframe.setAttribute('sandbox', sandbox);
  t.iframe.setAttribute('allow', allow);
  showNotice('Updated permissions for this tab.');
};

$('#btnNewTab').onclick = ()=> createTab('about:blank');

// Global keys
addEventListener('keydown', (e)=>{
  const meta = e.metaKey || e.ctrlKey;
  if (meta && e.key.toLowerCase()==='l'){ e.preventDefault(); $('#address').focus(); $('#address').select(); }
  if (meta && e.key.toLowerCase()==='t'){ e.preventDefault(); createTab('about:blank'); }
  if (meta && e.key.toLowerCase()==='w'){ e.preventDefault(); closeTab(App.activeId); }
  if ((e.altKey && e.key==='ArrowLeft') || (meta && e.key==='[')){ e.preventDefault(); back(); }
  if ((e.altKey && e.key==='ArrowRight') || (meta && e.key===']')){ e.preventDefault(); forward(); }
  if (meta && (e.key.toLowerCase()==='r')){ e.preventDefault(); reload(); }
});

// Close tab with middle click on tabbar
$('#tabbar').addEventListener('auxclick', (e)=>{
  const tabEl = e.target.closest('.tab'); if (!tabEl) return;
  if (e.button===1){ // middle click
    const idx = $$('.tab').indexOf(tabEl);
    const id = App.tabs[idx]?.id; if (id) closeTab(id);
  }
});

function showNotice(msg, ms=2200){
  const n = $('#notice'); n.textContent = msg; n.style.display='block';
  clearTimeout(showNotice._t); showNotice._t=setTimeout(()=>{ n.style.display='none'; }, ms);
}

/* ========= Boot ========= */
(function boot(){
  // Restore saved tabs or start fresh
  loadTabs();
  if (!App.tabs.length){
    createTab('data:text/html,' + encodeURIComponent(START_HTML('Try typing “example.com” or a search.')));
  }

  // Activate the last tab
  setActive(App.tabs[App.tabs.length-1].id);

  // If user types in the *embedded* start page, pressing Enter will navigate the iframe out of data:.
  // We update our address bar on iframe navigation we initiate, not internal links (cross-origin limits).
})();
</script>
</body>
</html>