diff --git a/.gitignore b/.gitignore
index d3e45bb..6f0e289 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,4 @@ env.json
# Song collection and import manifest (large, private)
/songs/
/songs_manifest.csv
+site/config.js
diff --git a/site/app.css b/site/app.css
new file mode 100644
index 0000000..54d948c
--- /dev/null
+++ b/site/app.css
@@ -0,0 +1,186 @@
+/* Nanga Kathai marketing site. Colours match lib/app/theme.dart (NkColors). */
+:root {
+ --ground: #0B1A14;
+ --surface: #14281F;
+ --raised: #1F3A2B;
+ --outline: #2A4537;
+ --emblem: #1F4A2E;
+ --tea: #7FA857;
+ --sun: #F5B335;
+ --cream: #FBF7EE;
+ --muted: #9FB3A6;
+ --on-photo: #DCE5DF;
+}
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ margin: 0; background: var(--ground); color: var(--on-photo);
+ font: 17px/1.65 Manrope, system-ui, -apple-system, "Segoe UI", sans-serif;
+ -webkit-font-smoothing: antialiased; overflow-x: hidden;
+}
+h1, h2, h3 { font-family: Sora, Manrope, sans-serif; color: var(--cream); line-height: 1.12; margin: 0; letter-spacing: -.015em; }
+h2 { font-size: clamp(28px, 4vw, 44px); }
+a { color: var(--sun); }
+img { display: block; max-width: 100%; }
+button { font: inherit; color: inherit; cursor: pointer; }
+.wrap { max-width: 1160px; margin: 0 auto; padding: 0 20px; }
+.muted { color: var(--muted); }
+.eyebrow { color: var(--sun); font-weight: 800; letter-spacing: .16em; text-transform: uppercase; font-size: 13px; margin: 0 0 14px; }
+section { padding: 96px 0 0; scroll-margin-top: 40px; }
+.center-head { text-align: center; max-width: 680px; margin: 0 auto 44px; }
+.center-head p.muted { margin: 14px 0 0; }
+
+/* Header */
+.top { position: sticky; top: 0; z-index: 20; background: rgba(11, 26, 20, .82); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(42, 69, 55, .5); }
+.top .wrap { display: flex; align-items: center; gap: 20px; height: 68px; }
+.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--cream); font: 700 18px Sora, sans-serif; }
+.brand img { width: 36px; height: 36px; border-radius: 9px; }
+.top nav { display: flex; align-items: center; gap: 26px; margin-left: auto; }
+.top nav a:not(.btn) { color: var(--on-photo); text-decoration: none; font-weight: 600; font-size: 15px; }
+.top nav a:not(.btn):hover { color: var(--sun); }
+@media (max-width: 760px) { .top nav a.opt { display: none; } }
+
+/* Buttons */
+.btn { display: inline-flex; align-items: center; gap: 10px; border: 0; border-radius: 999px; padding: 14px 26px; font-weight: 700; font-size: 16px; text-decoration: none; }
+.btn.sm { padding: 9px 18px; font-size: 14px; }
+.btn.primary { background: var(--sun); color: var(--ground); }
+.btn.primary:hover { background: #ffc552; }
+.btn.ghost { background: rgba(251,247,238,.08); color: var(--cream); border: 1px solid rgba(251,247,238,.25); }
+.btn.ghost:hover { background: rgba(251,247,238,.16); }
+.link { color: var(--sun); font-weight: 700; text-decoration: none; }
+.actions { display: flex; flex-wrap: wrap; gap: 12px; }
+.store-badge {
+ display: inline-flex; align-items: center; gap: 12px; padding: 10px 22px 10px 18px; border-radius: 14px;
+ background: var(--cream); color: var(--ground); text-decoration: none; font: 700 18px/1.15 Sora, sans-serif; min-width: 190px;
+}
+.store-badge svg { width: 26px; height: 26px; flex: none; }
+.store-badge small { display: block; font: 600 11px Manrope, sans-serif; opacity: .7; }
+.store-badge:hover { background: #fff; }
+.store-badge.soon { cursor: default; }
+.store-badge.soon:hover { background: var(--cream); }
+
+/* Hero */
+.hero { position: relative; padding: 72px 0 96px; overflow: hidden; }
+.hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: .55; }
+.hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(100deg, var(--ground) 20%, rgba(11,26,20,.6) 60%, rgba(11,26,20,.35)), linear-gradient(180deg, transparent 70%, var(--ground)); }
+.hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: 1.15fr .85fr; gap: 48px; align-items: center; }
+.hero h1 { font-size: clamp(40px, 6vw, 72px); }
+.hero h1 em { font-style: normal; color: var(--sun); }
+.lead { font-size: clamp(18px, 2vw, 21px); max-width: 580px; margin: 22px 0 32px; }
+.facts { display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 36px; color: var(--muted); font-size: 15px; }
+.facts b { color: var(--cream); font: 700 24px Sora, sans-serif; margin-right: 4px; }
+@media (max-width: 860px) { .hero-grid { grid-template-columns: 1fr; } .phone { margin: 0 auto; } }
+
+/* Phone preview */
+.phone {
+ width: 300px; border-radius: 44px; padding: 12px; background: #050d09;
+ box-shadow: 0 40px 80px rgba(0,0,0,.55), inset 0 0 0 2px #2a3a31; justify-self: center; transform: rotate(2deg);
+}
+.phone-screen { position: relative; border-radius: 34px; overflow: hidden; background: var(--ground); padding: 28px 22px 26px; text-align: center; }
+.phone-photo { position: absolute; inset: 0 0 45% 0; }
+.phone-photo img { width: 100%; height: 100%; object-fit: cover; opacity: .5; }
+.phone-photo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(11,26,20,.2), var(--ground)); }
+.phone-screen > *:not(.phone-photo) { position: relative; }
+.phone-label { margin: 0 0 18px; font-size: 11px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; color: var(--on-photo); }
+.phone-art { width: 196px; margin: 0 auto 20px; }
+.phone-art .cover { border-radius: 16px; box-shadow: 0 16px 40px rgba(0,0,0,.5); }
+.phone-art .cover span { font-size: 64px; }
+.phone-screen > b { display: block; color: var(--cream); font: 700 19px Sora, sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.phone-screen > span { display: block; color: var(--muted); font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.phone-progress { height: 4px; border-radius: 4px; background: rgba(251,247,238,.18); margin: 20px 0 6px; overflow: hidden; }
+.phone-progress i { display: block; height: 100%; width: 0; background: var(--sun); }
+.phone-times { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
+.phone-controls { display: flex; justify-content: center; align-items: center; gap: 22px; margin-top: 10px; }
+.phone-controls button { background: none; border: 0; color: var(--cream); padding: 6px; display: grid; place-items: center; border-radius: 999px; }
+.phone-controls button svg { width: 26px; height: 26px; }
+.phone-controls .big { width: 60px; height: 60px; background: var(--sun); color: var(--ground); }
+.phone-controls .big:hover { background: #ffc552; }
+
+/* Covers */
+.cover { position: relative; aspect-ratio: 1; border-radius: 10px; overflow: hidden; display: grid; place-items: center; }
+.cover::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 6%; min-height: 3px; background: var(--cream); opacity: .9; }
+.cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.cover span { color: rgba(251,247,238,.75); font-size: 22px; }
+
+/* Song samples */
+.listen-grid { display: grid; grid-template-columns: .8fr 1.2fr; gap: 56px; align-items: center; }
+.listen-grid h2 { margin-bottom: 16px; }
+.listen-grid p { margin: 0 0 20px; }
+@media (max-width: 860px) { .listen-grid { grid-template-columns: 1fr; gap: 28px; } }
+.samples { list-style: none; margin: 0; padding: 10px; background: var(--surface); border: 1px solid var(--outline); border-radius: 22px; }
+.sample { display: grid; grid-template-columns: 40px 48px minmax(0, 1fr) auto auto; gap: 14px; align-items: center; padding: 10px; border-radius: 14px; cursor: pointer; }
+.sample:hover, .sample.playing { background: var(--raised); }
+.sample-btn { width: 40px; height: 40px; border-radius: 999px; border: 0; background: rgba(251,247,238,.1); color: var(--cream); display: grid; place-items: center; }
+.sample:hover .sample-btn, .sample.playing .sample-btn { background: var(--sun); color: var(--ground); }
+.sample-btn svg { width: 18px; height: 18px; }
+.sample .t { min-width: 0; }
+.sample b { display: block; color: var(--cream); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.sample.playing b { color: var(--sun); }
+.sample .t span { display: block; font-size: 14px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.tag { font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: color-mix(in srgb, var(--c) 35%, transparent); color: var(--cream); white-space: nowrap; }
+.dur { font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }
+@media (max-width: 560px) { .sample { grid-template-columns: 40px 44px minmax(0, 1fr) auto; } .sample .tag { display: none; } }
+.skeleton { list-style: none; height: 64px; margin: 6px 0; border-radius: 14px; background: linear-gradient(90deg, var(--surface), var(--raised), var(--surface)); background-size: 200% 100%; animation: sk 1.4s infinite; }
+@keyframes sk { to { background-position: -200% 0; } }
+.empty { color: var(--muted); padding: 20px; }
+
+/* Occasions */
+.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
+.tile { position: relative; overflow: hidden; border-radius: 18px; padding: 24px 22px 30px; background: var(--c); min-height: 160px; }
+.tile:first-child { grid-row: span 2; display: flex; flex-direction: column; justify-content: flex-end; }
+.tile::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 6px; background: var(--cream); opacity: .9; }
+.tile b { display: block; font: 700 22px Sora, sans-serif; color: var(--cream); margin-bottom: 6px; }
+.tile span { font-size: 15px; color: rgba(251,247,238,.85); }
+@media (max-width: 860px) { .tiles { grid-template-columns: repeat(2, 1fr); } .tile:first-child { grid-row: auto; } }
+
+/* Features */
+.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
+.feature { background: var(--surface); border: 1px solid var(--outline); border-radius: 20px; padding: 28px; }
+.feature i { display: grid; place-items: center; width: 46px; height: 46px; border-radius: 14px; background: rgba(245,179,53,.14); color: var(--sun); font-style: normal; font-size: 22px; margin-bottom: 18px; }
+.feature b { display: block; color: var(--cream); font: 700 19px Sora, sans-serif; margin-bottom: 8px; }
+.feature p { margin: 0; color: var(--muted); font-size: 16px; }
+@media (max-width: 860px) { .features { grid-template-columns: 1fr 1fr; } }
+@media (max-width: 560px) { .features { grid-template-columns: 1fr; } }
+
+/* Story */
+.story { display: grid; grid-template-columns: 1fr 1.1fr; gap: 56px; align-items: center; }
+.story img { border-radius: 24px; width: 100%; aspect-ratio: 4 / 5; object-fit: cover; }
+.story h2 { margin-bottom: 20px; }
+.story p { margin: 0 0 16px; }
+@media (max-width: 860px) { .story { grid-template-columns: 1fr; } .story img { aspect-ratio: 16 / 10; } }
+
+/* Places */
+.places { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; max-width: 860px; margin: 0 auto; }
+.places span { padding: 8px 18px; border-radius: 999px; border: 1px solid var(--outline); background: var(--surface); color: var(--cream); font-weight: 600; font-size: 15px; }
+.places span:nth-child(-n+4) { border-color: rgba(127,168,87,.6); color: var(--tea); }
+
+/* Creators */
+.creator-band { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 24px; padding: 40px 44px; border-radius: 24px; border: 1px dashed rgba(245,179,53,.5); background: rgba(245,179,53,.05); }
+.creator-band > div { max-width: 640px; }
+.creator-band h2 { font-size: clamp(24px, 3vw, 34px); margin-bottom: 10px; }
+.creator-band p { margin: 0; }
+
+/* FAQ */
+.faq { max-width: 820px; }
+.faq details { border-bottom: 1px solid var(--outline); }
+.faq summary { cursor: pointer; list-style: none; padding: 22px 40px 22px 0; position: relative; color: var(--cream); font: 600 19px Sora, sans-serif; }
+.faq summary::-webkit-details-marker { display: none; }
+.faq summary::after { content: "+"; position: absolute; right: 4px; top: 50%; transform: translateY(-50%); color: var(--sun); font-size: 26px; font-weight: 400; }
+.faq details[open] summary::after { content: "\2212"; }
+.faq details p { margin: 0 0 22px; color: var(--on-photo); }
+
+/* Download */
+.app-band { border-radius: 28px; padding: 56px; background: radial-gradient(circle at 85% 20%, rgba(245,179,53,.18), transparent 50%), linear-gradient(120deg, var(--emblem), var(--surface)); display: grid; grid-template-columns: auto 1fr; gap: 36px; align-items: center; }
+.app-band .icon { width: 128px; height: 128px; border-radius: 30px; box-shadow: 0 14px 40px rgba(0,0,0,.4); }
+.app-band p { margin: 10px 0 24px; font-size: 19px; }
+@media (max-width: 700px) { .app-band { grid-template-columns: 1fr; padding: 32px; } .app-band .icon { width: 96px; height: 96px; } }
+
+/* Footer */
+footer { margin-top: 96px; border-top: 1px solid var(--outline); padding: 36px 0 44px; font-size: 14px; color: var(--muted); }
+footer .wrap { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px 28px; }
+footer .brand { font-size: 16px; }
+footer .brand img { width: 28px; height: 28px; border-radius: 7px; }
+footer nav { display: flex; flex-wrap: wrap; gap: 20px; }
+footer nav a { color: var(--on-photo); text-decoration: none; }
+footer nav a:hover { color: var(--sun); }
+footer .copy { width: 100%; }
diff --git a/site/app.js b/site/app.js
new file mode 100644
index 0000000..8a1098b
--- /dev/null
+++ b/site/app.js
@@ -0,0 +1,161 @@
+// Nanga Kathai marketing site. Reads a few songs and the catalogue counts from the live Supabase
+// backend (same tables as lib/data/catalog_repository.dart; row-level security only returns
+// approved songs) and plays them in the browser. Settings come from config.js (tools/build_site.py).
+(() => {
+ 'use strict';
+ const cfg = window.NK_CONFIG || {};
+ const API = `${cfg.supabaseUrl}/rest/v1`;
+ const headers = { apikey: cfg.supabaseKey, Authorization: `Bearer ${cfg.supabaseKey}` };
+ const FEATURED = 6;
+
+ // Same occasion labels and colours as lib/core/models.dart.
+ const OCCASIONS = {
+ huttu: ['Huttu', '#3B6E8F'],
+ habba: ['Habba', '#B5541E'],
+ madhuvae: ['Madhuvae', '#9C2F4A'],
+ devotional: ['Devotional', '#6B4FA0'],
+ folk: ['Folk songs', '#2E7D5B'],
+ saavu: ['Saavu', '#4A4F57'],
+ instrumental: ['Instrumental', '#8A5A2B'],
+ };
+
+ const $ = (sel, el = document) => el.querySelector(sel);
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+ const fmt = (sec) => `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, '0')}`;
+ const icons = {
+ play: ' ',
+ pause: ' ',
+ };
+
+ async function get(path, extra = {}) {
+ const res = await fetch(`${API}/${path}`, { headers: { ...headers, ...extra } });
+ if (!res.ok) throw new Error(`${res.status} ${path}`);
+ return res;
+ }
+
+ // One song from each of the biggest albums, so the sample shows some variety.
+ async function loadSongs() {
+ const [artists, albums, tracks] = await Promise.all([
+ get('artists?select=id,name_latin').then((r) => r.json()),
+ get('albums?select=id,title_latin,cover_url').then((r) => r.json()),
+ get('tracks?select=id,title_latin,album_id,occasion,duration_ms,audio_key_128,cover_url,'
+ + 'track_artists(artist_id,position,role)&status=eq.approved&order=created_at.asc').then((r) => r.json()),
+ ]);
+ const artistName = new Map(artists.map((a) => [a.id, a.name_latin]));
+ const albumById = new Map(albums.map((a) => [a.id, a]));
+ const byAlbum = new Map();
+ for (const t of tracks) {
+ if (!byAlbum.has(t.album_id)) byAlbum.set(t.album_id, []);
+ byAlbum.get(t.album_id).push(t);
+ }
+ const picks = [...byAlbum.values()].sort((a, b) => b.length - a.length).slice(0, FEATURED).map((list) => list[0]);
+ const songs = picks.map((r) => {
+ const album = albumById.get(r.album_id);
+ const singers = (r.track_artists || []).filter((t) => t.role === 'singer')
+ .sort((a, b) => a.position - b.position).map((t) => artistName.get(t.artist_id)).filter(Boolean);
+ const [label, color] = OCCASIONS[r.occasion] || OCCASIONS.folk;
+ return {
+ id: r.id,
+ title: r.title_latin,
+ credit: singers.join(', ') || album?.title_latin || 'Badaga song',
+ album: album?.title_latin,
+ label,
+ color,
+ coverUrl: r.cover_url || album?.cover_url || null,
+ audioUrl: `${cfg.mediaBaseUrl}/${r.audio_key_128}`,
+ seconds: (r.duration_ms || 0) / 1000,
+ };
+ });
+ return { songs, songCount: tracks.length, albumCount: byAlbum.size };
+ }
+
+ const cover = (s) => `
${s.coverUrl
+ ? `
` : '
♪ '}
`;
+
+ // ---------- Player ----------
+ const audio = new Audio();
+ audio.preload = 'none';
+ let songs = [];
+ let current = -1;
+
+ function play(i) {
+ if (i === current) { audio.paused ? audio.play() : audio.pause(); return; }
+ current = i;
+ const s = songs[i];
+ audio.src = s.audioUrl;
+ audio.play().catch(() => {});
+ $('#phone-title').textContent = s.title;
+ $('#phone-credit').textContent = s.credit;
+ $('#phone-art').innerHTML = cover(s);
+ if ('mediaSession' in navigator) {
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title: s.title, artist: s.credit, album: s.album || 'Nanga Kathai',
+ artwork: [{ src: s.coverUrl || '/img/icon-256.png', sizes: '256x256' }],
+ });
+ }
+ sync();
+ }
+
+ function sync() {
+ document.querySelectorAll('.sample').forEach((el, i) => {
+ const on = i === current && !audio.paused;
+ el.classList.toggle('playing', i === current);
+ el.querySelector('.sample-btn').innerHTML = on ? icons.pause : icons.play;
+ el.querySelector('.sample-btn').setAttribute('aria-label', `${on ? 'Pause' : 'Play'} ${songs[i].title}`);
+ });
+ const btn = $('#phone-play');
+ btn.innerHTML = audio.paused ? icons.play : icons.pause;
+ btn.setAttribute('aria-label', audio.paused ? 'Play' : 'Pause');
+ }
+
+ audio.addEventListener('play', sync);
+ audio.addEventListener('pause', sync);
+ audio.addEventListener('ended', () => (current < songs.length - 1 ? play(current + 1) : sync()));
+ audio.addEventListener('timeupdate', () => {
+ if (!audio.duration) return;
+ $('#phone-bar').style.width = `${(audio.currentTime / audio.duration) * 100}%`;
+ $('#phone-time').textContent = fmt(audio.currentTime);
+ $('#phone-total').textContent = fmt(audio.duration);
+ });
+
+ $('#phone-play').onclick = () => play(current < 0 ? 0 : current);
+ $('#phone-next').onclick = () => songs.length && play((current + 1) % songs.length);
+ $('#phone-prev').onclick = () => songs.length && play(current <= 0 ? 0 : current - 1);
+
+ loadSongs().then((data) => {
+ songs = data.songs;
+ $('#count-songs').textContent = data.songCount;
+ $('#count-albums').textContent = data.albumCount;
+ $('#samples').innerHTML = songs.map((s, i) => `
+
+ ${icons.play}
+ ${cover(s)}
+ ${esc(s.title)} ${esc(s.credit)}
+ ${esc(s.label)}
+ ${s.seconds ? fmt(s.seconds) : ''}
+ `).join('');
+ $('#samples').querySelectorAll('.sample').forEach((el, i) => { el.onclick = () => play(i); });
+ if (songs[0]) {
+ $('#phone-title').textContent = songs[0].title;
+ $('#phone-credit').textContent = songs[0].credit;
+ $('#phone-art').innerHTML = cover(songs[0]);
+ $('#phone-total').textContent = fmt(songs[0].seconds);
+ }
+ }).catch((err) => {
+ console.error(err);
+ $('#samples').innerHTML = 'The songs could not be loaded right now. Please refresh the page. ';
+ });
+
+ // ---------- Store links: "coming soon" until the URLs are set in config.js ----------
+ document.querySelectorAll('[data-store]').forEach((a) => {
+ const url = cfg[a.dataset.store];
+ if (url) {
+ a.href = url;
+ a.classList.remove('soon');
+ a.querySelector('small').textContent = a.dataset.store === 'playUrl' ? 'Get it on' : 'Download on the';
+ } else {
+ a.removeAttribute('href');
+ }
+ });
+ $('#year').textContent = new Date().getFullYear();
+})();
diff --git a/site/favicon.png b/site/favicon.png
new file mode 100644
index 0000000..e73e349
Binary files /dev/null and b/site/favicon.png differ
diff --git a/site/img/apple-touch-icon.png b/site/img/apple-touch-icon.png
new file mode 100644
index 0000000..46373f8
Binary files /dev/null and b/site/img/apple-touch-icon.png differ
diff --git a/site/img/icon-256.png b/site/img/icon-256.png
new file mode 100644
index 0000000..b2036e1
Binary files /dev/null and b/site/img/icon-256.png differ
diff --git a/site/img/nilgiris_tea.jpg b/site/img/nilgiris_tea.jpg
new file mode 100644
index 0000000..76341c2
Binary files /dev/null and b/site/img/nilgiris_tea.jpg differ
diff --git a/site/img/nilgiris_valley.jpg b/site/img/nilgiris_valley.jpg
new file mode 100644
index 0000000..7d5e89b
Binary files /dev/null and b/site/img/nilgiris_valley.jpg differ
diff --git a/site/img/nilgiris_village.jpg b/site/img/nilgiris_village.jpg
new file mode 100644
index 0000000..f77aefe
Binary files /dev/null and b/site/img/nilgiris_village.jpg differ
diff --git a/site/img/og.jpg b/site/img/og.jpg
new file mode 100644
index 0000000..36d78c8
Binary files /dev/null and b/site/img/og.jpg differ
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..c2ccee1
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,253 @@
+
+
+
+
+
+Nanga Kathai: Badaga Songs App, Free with No Ads
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The Badaga music app
+
Songs of the Nilgiris, in your pocket
+
Nanga Kathai brings Badaga folk, wedding, festival and devotional songs together in one free app. No ads. Listen online or offline, wherever you are.
+
+
180+ songs18 albums7 occasions0 ads
+
+
+
+
+
+
Now playing
+
+
Badaga songs
+
Nanga Kathai
+
+
0:00 0:00
+
+
+
+
+
+
+
+
+
+
Hear it now
+
A taste of what's inside
+
A few songs from the collection. Press play right here, then get the app for every song, offline downloads and your own playlists.
+
Get the full collection →
+
+
+
+
+
+
+
+
+
For every moment
+
The songs that mark a Badaga life
+
+
+
Huttu Songs for a birth and the naming day.
+
Habba Festival songs, from the Hethai Habba to harvest.
+
Madhuvae Wedding songs sung by family and friends.
+
Devotional Songs for Hethai and the temples of the hills.
+
Folk Traditional melodies from the hattis and tea fields.
+
Saavu Songs of remembrance for those we have lost.
+
Instrumental Flute, drums and the sounds of celebration.
+
+
+
+
+
+
+
+
Why Nanga Kathai
+
Made for Badaga listeners
+
+
+
✦ Free, with no ads Every song is free. No ads interrupting a wedding song, now or ever.
+
↧ Listen offline Download songs and play them anywhere, even with no signal on the ghat road or a flight to Dubai.
+
⚲ Find any song Search in English or Tamil script. Spelling is forgiving, because Badaga words are written many ways.
+
♫ Your own playlists Like songs and build playlists for the next function, festival or long drive.
+
◉ Plays in the background Lock-screen controls, Bluetooth speakers and car audio all work.
+
❤ Respecting singers Every song can be claimed by its singer or family, for credit or removal.
+
+
+
+
+
+
+
+
+
Our story
+
Nanga Kathai means "our story"
+
For generations the Badagas of the Nilgiris have sung their stories: at weddings and naming days, at the Hethai Habba, in the tea fields, and in remembrance. Many of those songs survive only on old cassettes in family homes.
+
Nanga Kathai gathers them in one place, so Badaga families in the hattis, in Coimbatore and Bengaluru, and across the Gulf, Canada and the US can hear them any time and pass them on to their children.
+
It is built by Metatroncube Software Solutions, founded by a Badaga, as a way of giving back to the community.
+
+
+
+
+
+
+
+
Wherever you are
+
From the Nilgiris to the world
+
Badaga families now live far from the hills. Nanga Kathai keeps home a tap away.
+
+
+ Ooty Kotagiri Coonoor Gudalur Coimbatore Bengaluru Chennai
+ Dubai Abu Dhabi Doha Riyadh Kuwait Muscat
+ Toronto Vancouver New Jersey California London Singapore Sydney
+
+
+
+
+
+
+
+
+
Coming soon
+
Are you a Badaga singer?
+
Creator accounts are on the way. You'll be able to publish your own songs and receive support directly from listeners around the world.
+
+
Tell me when it opens
+
+
+
+
+
+
+
Questions
Frequently asked
+
Is Nanga Kathai free? Yes. Every song is free to listen to and there are no ads. If you'd like to, you can send a one-time gift in the app to support its development, but it unlocks nothing extra.
+
Can I listen to Badaga songs offline? Yes. Download songs in the app and play them without an internet connection.
+
Can I use it outside India? Yes. Nanga Kathai works worldwide, including the Gulf countries, Canada, the US, the UK and Australia.
+
One of the songs is mine or my family's. What can I do? Tap "Is this your song?" in the app, or email support@nkbmusic.com . We'll credit you properly, or remove the song if you prefer.
+
I am a Badaga singer. Can I publish my songs? Creator accounts are coming in a future update. Email us and we'll tell you when it opens.
+
+
+
+
+
+
+
+
+
Take Badaga music with you
+
Free on Android and iPhone. No ads, ever.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/site/robots.txt b/site/robots.txt
new file mode 100644
index 0000000..5a213cc
--- /dev/null
+++ b/site/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://nkbmusic.com/sitemap.xml
diff --git a/site/sitemap.xml b/site/sitemap.xml
new file mode 100644
index 0000000..8124fb7
--- /dev/null
+++ b/site/sitemap.xml
@@ -0,0 +1,7 @@
+
+
+ https://nkbmusic.com/ 2026-10-31 1.0
+ https://nkbmusic.com/privacy 2026-10-31 0.3
+ https://nkbmusic.com/terms 2026-10-31 0.3
+ https://nkbmusic.com/delete-account 2026-10-31 0.3
+
diff --git a/tools/build_site.py b/tools/build_site.py
new file mode 100644
index 0000000..0fe97d1
--- /dev/null
+++ b/tools/build_site.py
@@ -0,0 +1,48 @@
+"""Prepare the nkbmusic.com website in site/ for upload.
+
+Writes site/config.js (backend URL and public key, from env.json) and site/sitemap.xml
+(home and legal pages).
+
+ python tools/build_site.py [--play-url URL] [--app-store-url URL]
+"""
+import argparse
+import datetime
+import json
+import pathlib
+
+ROOT = pathlib.Path(__file__).resolve().parent.parent
+SITE = ROOT / 'site'
+BASE = 'https://nkbmusic.com'
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--play-url', default='', help='Google Play link, once the app is live')
+ ap.add_argument('--app-store-url', default='', help='App Store link, once the app is live')
+ args = ap.parse_args()
+
+ env = json.loads((ROOT / 'env.json').read_text(encoding='utf-8'))
+ config = {
+ 'supabaseUrl': env['SUPABASE_URL'],
+ # The publishable (anon) key is public by design; row-level security protects the data.
+ 'supabaseKey': env['SUPABASE_PUBLISHABLE_KEY'],
+ 'mediaBaseUrl': env['MEDIA_BASE_URL'],
+ 'playUrl': args.play_url,
+ 'appStoreUrl': args.app_store_url,
+ }
+ (SITE / 'config.js').write_text(f'window.NK_CONFIG = {json.dumps(config, indent=2)};\n', encoding='utf-8')
+
+ today = datetime.date.today().isoformat()
+ urls = [(f'{BASE}/', '1.0'), (f'{BASE}/privacy', '0.3'), (f'{BASE}/terms', '0.3'),
+ (f'{BASE}/delete-account', '0.3')]
+ body = '\n'.join(f' {u} {today} {p} '
+ for u, p in urls)
+ (SITE / 'sitemap.xml').write_text(
+ '\n'
+ f'\n{body}\n \n', encoding='utf-8')
+ (SITE / 'robots.txt').write_text(f'User-agent: *\nAllow: /\n\nSitemap: {BASE}/sitemap.xml\n', encoding='utf-8')
+ print(f'config.js, sitemap.xml ({len(urls)} URLs) and robots.txt written to {SITE}')
+
+
+if __name__ == '__main__':
+ main()