adding details
This commit is contained in:
23
frontend/src/App.vue
Normal file
23
frontend/src/App.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import TheHeader from '@/components/TheHeader.vue'
|
||||
import TheFooter from '@/components/TheFooter.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TheHeader />
|
||||
<main>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<transition name="route-fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</RouterView>
|
||||
</main>
|
||||
<TheFooter />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
flex: 1;
|
||||
padding-top: var(--nav-height);
|
||||
}
|
||||
</style>
|
||||
32
frontend/src/api/client.js
Normal file
32
frontend/src/api/client.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// In dev, leave VITE_API_BASE_URL empty and let Vite proxy /api → Django.
|
||||
// In prod, set VITE_API_BASE_URL=https://api.risingcompute.in (or same origin).
|
||||
const baseURL = (import.meta.env.VITE_API_BASE_URL || '') + '/api'
|
||||
|
||||
const client = axios.create({
|
||||
baseURL,
|
||||
timeout: 15000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
export const api = {
|
||||
// Content
|
||||
listProducts: () => client.get('/products/').then(r => r.data),
|
||||
getProduct: (slug) => client.get(`/products/${slug}/`).then(r => r.data),
|
||||
listFounders: () => client.get('/founders/').then(r => r.data),
|
||||
listPosts: () => client.get('/posts/').then(r => r.data),
|
||||
getPost: (slug) => client.get(`/posts/${slug}/`).then(r => r.data),
|
||||
listJobs: () => client.get('/jobs/').then(r => r.data),
|
||||
|
||||
// Submissions
|
||||
submitContact: (payload) => client.post('/contact/', payload).then(r => r.data),
|
||||
subscribeNewsletter: (email, source = '') =>
|
||||
client.post('/newsletter/', { email, source }).then(r => r.data),
|
||||
submitApplication: (formData) =>
|
||||
client.post('/apply/', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}).then(r => r.data),
|
||||
}
|
||||
|
||||
export default client
|
||||
342
frontend/src/assets/styles/main.css
Normal file
342
frontend/src/assets/styles/main.css
Normal file
@@ -0,0 +1,342 @@
|
||||
/* ===================================================================
|
||||
* RisingCompute — Design tokens & base styles
|
||||
* Brand: Fast · Reliable · Robust
|
||||
* Dark theme by default. Indigo + Cyan + minimal motion.
|
||||
* =================================================================== */
|
||||
|
||||
:root {
|
||||
/* Brand palette — see design-document §7.2 */
|
||||
--color-bg: #0B1437; /* Deep Space Indigo */
|
||||
--color-bg-soft: #111B47;
|
||||
--color-surface: #1A2347; /* Slate Navy — cards */
|
||||
--color-surface-2: #232C57;
|
||||
--color-border: #2A3463;
|
||||
--color-border-soft: #1F2750;
|
||||
|
||||
--color-text: #F4F6FB; /* Mist */
|
||||
--color-text-muted: #B6BED2;
|
||||
--color-text-dim: #8A93A6; /* Steel */
|
||||
|
||||
--color-accent: #00E5FF; /* Accelerate Cyan */
|
||||
--color-accent-soft: rgba(0, 229, 255, 0.16);
|
||||
--color-accent-glow: rgba(0, 229, 255, 0.35);
|
||||
|
||||
--color-success: #22C55E;
|
||||
--color-warning: #F59E0B;
|
||||
--color-error: #EF4444;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
|
||||
/* Spacing scale */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.5rem;
|
||||
--space-6: 2rem;
|
||||
--space-7: 3rem;
|
||||
--space-8: 4rem;
|
||||
--space-9: 6rem;
|
||||
--space-10: 8rem;
|
||||
|
||||
/* Radii */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 22px;
|
||||
|
||||
/* Layout */
|
||||
--max-content: 1240px;
|
||||
--nav-height: 72px;
|
||||
|
||||
/* Motion */
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--transition-fast: 120ms var(--ease-out);
|
||||
--transition: 220ms var(--ease-out);
|
||||
--transition-slow: 420ms var(--ease-out);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(1100px 600px at 80% -10%, rgba(0, 229, 255, 0.08), transparent 60%),
|
||||
radial-gradient(900px 500px at 0% 0%, rgba(100, 120, 255, 0.06), transparent 60%),
|
||||
var(--color-bg);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ---------- Typography ---------- */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 var(--space-4);
|
||||
color: var(--color-text);
|
||||
}
|
||||
h1 { font-size: clamp(2.25rem, 5vw, 3.75rem); font-weight: 700; letter-spacing: -0.03em; }
|
||||
h2 { font-size: clamp(1.75rem, 3vw, 2.5rem); }
|
||||
h3 { font-size: 1.5rem; }
|
||||
h4 { font-size: 1.25rem; }
|
||||
|
||||
p { margin: 0 0 var(--space-4); color: var(--color-text-muted); }
|
||||
small { color: var(--color-text-dim); }
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
a:hover { color: var(--color-text); }
|
||||
|
||||
code, pre, kbd {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* ---------- Layout ---------- */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max-content);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-5);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--space-9) 0;
|
||||
}
|
||||
.section--tight { padding: var(--space-7) 0; }
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.eyebrow::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 1px;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1;
|
||||
padding: 0.85rem 1.4rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: var(--color-accent);
|
||||
color: #001722;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn--primary:hover {
|
||||
background: #5BF0FF;
|
||||
box-shadow: 0 8px 28px -8px var(--color-accent-glow);
|
||||
color: #001722;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
.btn--ghost:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.btn--link {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.btn--link:hover { color: var(--color-text); }
|
||||
|
||||
.btn--full { width: 100%; }
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
.card {
|
||||
background: linear-gradient(180deg, var(--color-surface) 0%, var(--color-surface-2) 100%);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
}
|
||||
.card:hover {
|
||||
border-color: rgba(0, 229, 255, 0.35);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* ---------- Forms ---------- */
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.field label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
background: var(--color-bg-soft);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.8rem 1rem;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.95rem;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.field input::placeholder,
|
||||
.field textarea::placeholder { color: var(--color-text-dim); }
|
||||
.field input:focus,
|
||||
.field select:focus,
|
||||
.field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
||||
}
|
||||
.field textarea {
|
||||
min-height: 140px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: var(--color-error);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.form-success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid rgba(34, 197, 94, 0.35);
|
||||
color: var(--color-success);
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ---------- Pills / Tags ---------- */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
border: 1px solid rgba(0, 229, 255, 0.25);
|
||||
}
|
||||
|
||||
/* ---------- Grids ---------- */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.grid--3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid--2 { grid-template-columns: repeat(2, 1fr); }
|
||||
@media (max-width: 900px) {
|
||||
.grid--3, .grid--2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ---------- Page transitions ---------- */
|
||||
.route-fade-enter-active,
|
||||
.route-fade-leave-active {
|
||||
transition: opacity 200ms var(--ease-out);
|
||||
}
|
||||
.route-fade-enter-from,
|
||||
.route-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ---------- Utility ---------- */
|
||||
.text-center { text-align: center; }
|
||||
.muted { color: var(--color-text-muted); }
|
||||
.dim { color: var(--color-text-dim); }
|
||||
.mono { font-family: var(--font-mono); }
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--color-border), transparent);
|
||||
margin: var(--space-7) 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Reduce motion for users who request it */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
||||
129
frontend/src/components/ContactForm.vue
Normal file
129
frontend/src/components/ContactForm.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { api } from '@/api/client'
|
||||
|
||||
const props = defineProps({
|
||||
defaultInterest: { type: String, default: 'other' },
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
role: '',
|
||||
country: '',
|
||||
interest: props.defaultInterest,
|
||||
message: '',
|
||||
referrer: typeof document !== 'undefined' ? document.referrer : '',
|
||||
})
|
||||
|
||||
const status = ref('idle') // idle | loading | success | error
|
||||
const errors = ref({})
|
||||
const errorMessage = ref('')
|
||||
|
||||
async function submit() {
|
||||
status.value = 'loading'
|
||||
errors.value = {}
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
await api.submitContact(form)
|
||||
status.value = 'success'
|
||||
} catch (err) {
|
||||
status.value = 'error'
|
||||
if (err.response?.data && typeof err.response.data === 'object') {
|
||||
errors.value = err.response.data
|
||||
errorMessage.value = 'Please check the fields below.'
|
||||
} else {
|
||||
errorMessage.value = 'Something went wrong. Please try again.'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="contact-form" @submit.prevent="submit" novalidate>
|
||||
<template v-if="status !== 'success'">
|
||||
<div class="form-row">
|
||||
<div class="field">
|
||||
<label for="cf-name">Name</label>
|
||||
<input id="cf-name" v-model="form.name" type="text" required autocomplete="name" />
|
||||
<span v-if="errors.name" class="form-error">{{ errors.name[0] }}</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="cf-email">Work email</label>
|
||||
<input id="cf-email" v-model="form.email" type="email" required autocomplete="email" />
|
||||
<span v-if="errors.email" class="form-error">{{ errors.email[0] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="field">
|
||||
<label for="cf-company">Company</label>
|
||||
<input id="cf-company" v-model="form.company" type="text" autocomplete="organization" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="cf-role">Role</label>
|
||||
<input id="cf-role" v-model="form.role" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="field">
|
||||
<label for="cf-country">Country</label>
|
||||
<input id="cf-country" v-model="form.country" type="text" autocomplete="country-name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="cf-interest">Interest</label>
|
||||
<select id="cf-interest" v-model="form.interest">
|
||||
<option value="ai-ip">AI Inference IP</option>
|
||||
<option value="security-ip">Cybersecurity IP</option>
|
||||
<option value="comms-ip">Communication IP</option>
|
||||
<option value="custom-asic">Custom ASIC</option>
|
||||
<option value="careers">Careers</option>
|
||||
<option value="press">Press</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="cf-message">What are you looking for?</label>
|
||||
<textarea
|
||||
id="cf-message"
|
||||
v-model="form.message"
|
||||
required
|
||||
placeholder="A few lines about the project, target node / FPGA, and timeline."
|
||||
></textarea>
|
||||
<span v-if="errors.message" class="form-error">{{ errors.message[0] }}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="form-error">{{ errorMessage }}</p>
|
||||
|
||||
<button type="submit" class="btn btn--primary btn--full" :disabled="status === 'loading'">
|
||||
{{ status === 'loading' ? 'Sending…' : 'Send enquiry' }}
|
||||
</button>
|
||||
<p class="dim contact-form__legal">
|
||||
We typically reply within one business day. By submitting, you agree to be
|
||||
contacted about your enquiry.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-else class="form-success">
|
||||
<h3>Thanks — message received.</h3>
|
||||
<p>One of the founders will follow up at <strong>{{ form.email }}</strong> within one business day.</p>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contact-form {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
.contact-form__legal {
|
||||
font-size: 0.78rem;
|
||||
margin: var(--space-3) 0 0;
|
||||
}
|
||||
</style>
|
||||
72
frontend/src/components/FounderCard.vue
Normal file
72
frontend/src/components/FounderCard.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
founder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
function initials(name) {
|
||||
return name.split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="founder">
|
||||
<div class="founder__avatar" :aria-hidden="!!founder.photo_url">
|
||||
<img v-if="founder.photo_url" :src="founder.photo_url" :alt="founder.name" />
|
||||
<span v-else>{{ initials(founder.name) }}</span>
|
||||
</div>
|
||||
<h3 class="founder__name">{{ founder.name }}</h3>
|
||||
<p class="founder__role">{{ founder.role }}</p>
|
||||
<p class="founder__domain mono">{{ founder.domain }}</p>
|
||||
<p class="founder__bio">{{ founder.bio }}</p>
|
||||
<a v-if="founder.linkedin_url" :href="founder.linkedin_url" target="_blank" rel="noopener" class="btn--link">
|
||||
LinkedIn ↗
|
||||
</a>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.founder {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
}
|
||||
.founder:hover {
|
||||
border-color: rgba(0, 229, 255, 0.35);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.founder__avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-accent-soft), rgba(11, 20, 55, 0.6));
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
}
|
||||
.founder__avatar img {
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
}
|
||||
.founder__name { margin: 0; font-size: 1.25rem; }
|
||||
.founder__role { color: var(--color-accent); font-size: 0.9rem; margin: 0; font-weight: 500; }
|
||||
.founder__domain {
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 0;
|
||||
}
|
||||
.founder__bio { color: var(--color-text-muted); margin: 0; }
|
||||
</style>
|
||||
75
frontend/src/components/ProductCard.vue
Normal file
75
frontend/src/components/ProductCard.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
defineProps({
|
||||
product: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const iconMap = {
|
||||
ai: '∿',
|
||||
security: '⌬',
|
||||
comms: '⤬',
|
||||
asic: '◇',
|
||||
other: '◍',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink :to="`/products/${product.slug}`" class="product">
|
||||
<div class="product__head">
|
||||
<span class="product__icon mono">{{ iconMap[product.category] || '◍' }}</span>
|
||||
<span class="pill">{{ product.category_label }}</span>
|
||||
</div>
|
||||
<h3 class="product__name">{{ product.name }}</h3>
|
||||
<p class="product__tagline">{{ product.tagline }}</p>
|
||||
<p class="product__summary">{{ product.summary }}</p>
|
||||
<span class="product__more">Read more →</span>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.product {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
color: var(--color-text);
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
height: 100%;
|
||||
}
|
||||
.product:hover {
|
||||
border-color: rgba(0, 229, 255, 0.4);
|
||||
transform: translateY(-3px);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.product__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.product__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px; height: 44px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
font-size: 1.5rem;
|
||||
border: 1px solid rgba(0, 229, 255, 0.25);
|
||||
}
|
||||
.product__name { margin: 0; font-size: 1.3rem; }
|
||||
.product__tagline { color: var(--color-accent); font-size: 0.95rem; margin: 0; }
|
||||
.product__summary { color: var(--color-text-muted); margin: 0; flex: 1; }
|
||||
.product__more {
|
||||
color: var(--color-accent);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
32
frontend/src/components/SectionHero.vue
Normal file
32
frontend/src/components/SectionHero.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
eyebrow: String,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-hero">
|
||||
<div class="container">
|
||||
<span v-if="eyebrow" class="eyebrow">{{ eyebrow }}</span>
|
||||
<h1>{{ title }}</h1>
|
||||
<p v-if="subtitle" class="page-hero__sub">{{ subtitle }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-hero {
|
||||
padding: var(--space-9) 0 var(--space-7);
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
background:
|
||||
radial-gradient(800px 360px at 90% 0%, rgba(0, 229, 255, 0.08), transparent 60%);
|
||||
}
|
||||
.page-hero__sub {
|
||||
font-size: 1.15rem;
|
||||
color: var(--color-text-muted);
|
||||
max-width: 720px;
|
||||
margin: var(--space-4) 0 0;
|
||||
}
|
||||
</style>
|
||||
187
frontend/src/components/TheFooter.vue
Normal file
187
frontend/src/components/TheFooter.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/api/client'
|
||||
|
||||
const email = ref('')
|
||||
const status = ref('idle') // idle | loading | success | error
|
||||
const message = ref('')
|
||||
|
||||
async function subscribe() {
|
||||
if (!email.value) return
|
||||
status.value = 'loading'
|
||||
try {
|
||||
await api.subscribeNewsletter(email.value, 'footer')
|
||||
status.value = 'success'
|
||||
message.value = "You're on the list."
|
||||
email.value = ''
|
||||
} catch (err) {
|
||||
status.value = 'error'
|
||||
message.value = 'Something went wrong. Try again in a moment.'
|
||||
}
|
||||
}
|
||||
|
||||
const year = new Date().getFullYear()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<div class="container footer__grid">
|
||||
<div class="footer__brand">
|
||||
<div class="brand-line">
|
||||
<svg width="28" height="28" viewBox="0 0 64 64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="fg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0B1437"/>
|
||||
<stop offset="100%" stop-color="#00E5FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="64" height="64" rx="14" fill="url(#fg)"/>
|
||||
<path d="M16 44 L26 28 L36 38 L48 18" stroke="#F4F6FB" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<strong>RisingCompute</strong>
|
||||
</div>
|
||||
<p class="footer__tagline">Accelerating the compute — IP cores for AI, space, and robotics.</p>
|
||||
<p class="footer__incub mono">Incubated at STIIC · IIST</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><RouterLink to="/about">About</RouterLink></li>
|
||||
<li><RouterLink to="/careers">Careers</RouterLink></li>
|
||||
<li><RouterLink to="/blog">Insights</RouterLink></li>
|
||||
<li><RouterLink to="/contact">Contact</RouterLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Products</h4>
|
||||
<ul>
|
||||
<li><RouterLink to="/products/ai-inference-ip">AI Inference IP</RouterLink></li>
|
||||
<li><RouterLink to="/products/cybersecurity-ip">Cybersecurity IP</RouterLink></li>
|
||||
<li><RouterLink to="/products/communication-ip">Communication IP</RouterLink></li>
|
||||
<li><RouterLink to="/technology">Technology</RouterLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer__news">
|
||||
<h4>Newsletter</h4>
|
||||
<p class="dim">Quarterly technical updates. No spam.</p>
|
||||
<form class="footer__sub" @submit.prevent="subscribe">
|
||||
<input
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@company.com"
|
||||
aria-label="Email address"
|
||||
/>
|
||||
<button type="submit" class="btn btn--primary" :disabled="status === 'loading'">
|
||||
{{ status === 'loading' ? '…' : 'Subscribe' }}
|
||||
</button>
|
||||
</form>
|
||||
<p v-if="status === 'success'" class="footer__msg footer__msg--ok">{{ message }}</p>
|
||||
<p v-if="status === 'error'" class="footer__msg footer__msg--err">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container footer__bottom">
|
||||
<p class="dim">© {{ year }} RisingCompute Pvt Ltd · Surat, Gujarat, India</p>
|
||||
<p class="dim">
|
||||
<RouterLink to="/contact">Privacy</RouterLink> ·
|
||||
<RouterLink to="/contact">Terms</RouterLink> ·
|
||||
<a href="mailto:contact@risingcompute.in">contact@risingcompute.in</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
border-top: 1px solid var(--color-border-soft);
|
||||
padding: var(--space-8) 0 var(--space-5);
|
||||
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.25));
|
||||
margin-top: var(--space-9);
|
||||
}
|
||||
|
||||
.footer__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr 1fr 1.4fr;
|
||||
gap: var(--space-7);
|
||||
margin-bottom: var(--space-7);
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.footer__grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.footer__grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.brand-line {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.footer__tagline { color: var(--color-text-muted); max-width: 280px; }
|
||||
.footer__incub { color: var(--color-text-dim); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
|
||||
.footer h4 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-dim);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.footer ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.footer ul a {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.footer ul a:hover { color: var(--color-accent); }
|
||||
|
||||
.footer__sub {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
.footer__sub input {
|
||||
flex: 1;
|
||||
background: var(--color-bg-soft);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.7rem 0.9rem;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.footer__sub input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
.footer__sub button { padding: 0.7rem 1rem; font-size: 0.9rem; }
|
||||
.footer__msg { font-size: 0.85rem; margin-top: var(--space-2); }
|
||||
.footer__msg--ok { color: var(--color-success); }
|
||||
.footer__msg--err { color: var(--color-error); }
|
||||
|
||||
.footer__bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: var(--space-5);
|
||||
border-top: 1px solid var(--color-border-soft);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.footer__bottom { flex-direction: column; gap: var(--space-2); }
|
||||
}
|
||||
.footer__bottom a { color: var(--color-text-muted); }
|
||||
.footer__bottom a:hover { color: var(--color-accent); }
|
||||
</style>
|
||||
160
frontend/src/components/TheHeader.vue
Normal file
160
frontend/src/components/TheHeader.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const scrolled = ref(false)
|
||||
const menuOpen = ref(false)
|
||||
|
||||
const onScroll = () => { scrolled.value = window.scrollY > 8 }
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
onScroll()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll))
|
||||
|
||||
const closeMenu = () => { menuOpen.value = false }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="nav" :class="{ 'nav--scrolled': scrolled }">
|
||||
<div class="container nav__inner">
|
||||
<RouterLink to="/" class="brand" @click="closeMenu">
|
||||
<svg width="32" height="32" viewBox="0 0 64 64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="logoGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0B1437"/>
|
||||
<stop offset="100%" stop-color="#00E5FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="64" height="64" rx="14" fill="url(#logoGrad)"/>
|
||||
<path d="M16 44 L26 28 L36 38 L48 18" stroke="#F4F6FB" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="brand__name">RisingCompute</span>
|
||||
</RouterLink>
|
||||
|
||||
<button
|
||||
class="nav__toggle"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-label="Toggle menu"
|
||||
@click="menuOpen = !menuOpen"
|
||||
>
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
|
||||
<nav class="nav__links" :class="{ 'nav__links--open': menuOpen }" @click="closeMenu">
|
||||
<RouterLink to="/products">Products</RouterLink>
|
||||
<RouterLink to="/technology">Technology</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
<RouterLink to="/careers">Careers</RouterLink>
|
||||
<RouterLink to="/blog">Insights</RouterLink>
|
||||
<RouterLink to="/contact" class="btn btn--primary nav__cta">Talk to us</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--nav-height);
|
||||
z-index: 50;
|
||||
background: rgba(11, 20, 55, 0.6);
|
||||
backdrop-filter: saturate(1.4) blur(14px);
|
||||
-webkit-backdrop-filter: saturate(1.4) blur(14px);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: background var(--transition), border-color var(--transition);
|
||||
}
|
||||
.nav--scrolled {
|
||||
background: rgba(11, 20, 55, 0.88);
|
||||
border-bottom-color: var(--color-border-soft);
|
||||
}
|
||||
|
||||
.nav__inner {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.brand:hover { color: var(--color-text); }
|
||||
.brand__name { font-size: 1.05rem; }
|
||||
|
||||
.nav__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.nav__links :where(a):not(.btn) {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.nav__links :where(a):not(.btn):hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.nav__links :where(a.router-link-active):not(.btn) {
|
||||
color: var(--color-accent);
|
||||
border-bottom-color: var(--color-accent);
|
||||
}
|
||||
.nav__cta { padding: 0.55rem 1rem; font-size: 0.9rem; }
|
||||
|
||||
.nav__toggle {
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
width: 40px;
|
||||
height: 36px;
|
||||
padding: 8px 10px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav__toggle span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--color-text);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.nav__toggle { display: flex; }
|
||||
.nav__links {
|
||||
position: absolute;
|
||||
top: var(--nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: var(--space-5);
|
||||
background: rgba(11, 20, 55, 0.98);
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
transform: translateY(-12px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.nav__links--open {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.nav__cta { margin-top: var(--space-2); }
|
||||
}
|
||||
</style>
|
||||
6
frontend/src/main.js
Normal file
6
frontend/src/main.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/styles/main.css'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
30
frontend/src/router/index.js
Normal file
30
frontend/src/router/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'home', component: () => import('@/views/HomeView.vue'), meta: { title: 'RisingCompute — Accelerating the compute' } },
|
||||
{ path: '/about', name: 'about', component: () => import('@/views/AboutView.vue'), meta: { title: 'About — RisingCompute' } },
|
||||
{ path: '/products', name: 'products', component: () => import('@/views/ProductsView.vue'), meta: { title: 'Products — RisingCompute' } },
|
||||
{ path: '/products/:slug', name: 'product-detail', component: () => import('@/views/ProductDetailView.vue'), meta: { title: 'Product — RisingCompute' } },
|
||||
{ path: '/technology', name: 'technology', component: () => import('@/views/TechnologyView.vue'), meta: { title: 'Technology — RisingCompute' } },
|
||||
{ path: '/careers', name: 'careers', component: () => import('@/views/CareersView.vue'), meta: { title: 'Careers — RisingCompute' } },
|
||||
{ path: '/blog', name: 'blog', component: () => import('@/views/BlogView.vue'), meta: { title: 'Insights — RisingCompute' } },
|
||||
{ path: '/blog/:slug', name: 'blog-post', component: () => import('@/views/BlogPostView.vue'), meta: { title: 'Insights — RisingCompute' } },
|
||||
{ path: '/contact', name: 'contact', component: () => import('@/views/ContactView.vue'), meta: { title: 'Talk to us — RisingCompute' } },
|
||||
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFoundView.vue'), meta: { title: 'Not found — RisingCompute' } },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, saved) {
|
||||
if (saved) return saved
|
||||
if (to.hash) return { el: to.hash, behavior: 'smooth' }
|
||||
return { top: 0 }
|
||||
},
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
if (to.meta?.title) document.title = to.meta.title
|
||||
})
|
||||
|
||||
export default router
|
||||
159
frontend/src/views/AboutView.vue
Normal file
159
frontend/src/views/AboutView.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
import SectionHero from '@/components/SectionHero.vue'
|
||||
import FounderCard from '@/components/FounderCard.vue'
|
||||
|
||||
const founders = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try { founders.value = await api.listFounders() }
|
||||
catch (e) {}
|
||||
finally { loading.value = false }
|
||||
})
|
||||
|
||||
const values = [
|
||||
{ name: 'Fast', body: 'We measure things. We ship blocks. We don\'t hide behind roadmaps.' },
|
||||
{ name: 'Reliable', body: 'Verification-first. Documentation that an integrator can actually use.' },
|
||||
{ name: 'Robust', body: 'Designed for orbits and factory floors — robustness is the baseline, not a feature.' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionHero
|
||||
eyebrow="About"
|
||||
title="Built by a satellite-software team, for everyone who needs that compute."
|
||||
subtitle="RisingCompute designs parallel hardware architectures and IP cores for AI, space, and robotics. The founders met in the SSPACE lab at IIST — and brought the same standards out into the open."
|
||||
/>
|
||||
|
||||
<!-- STORY -->
|
||||
<section class="section section--tight">
|
||||
<div class="container story">
|
||||
<div>
|
||||
<span class="eyebrow">Our story</span>
|
||||
<h2>Closing the compute gap.</h2>
|
||||
</div>
|
||||
<div class="story__body">
|
||||
<p>
|
||||
RisingCompute began in 2026 inside the SSPACE lab at IIST, where our founding
|
||||
team spent two years building computation systems for satellites — IP that has
|
||||
since flown in space.
|
||||
</p>
|
||||
<p>
|
||||
Working at that level taught us something simple: the difference between what a
|
||||
research team can do with AI and what most engineers and companies can do comes
|
||||
down to the compute they have access to. We started RisingCompute to close that
|
||||
gap. By building parallel hardware architectures and reusable IP cores, we make
|
||||
accelerated compute scalable and easy to integrate — so an aerospace OEM, a
|
||||
robotics startup, or a defense electronics lab can get research-grade
|
||||
performance without rebuilding the stack from scratch.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider container" />
|
||||
|
||||
<!-- FOUNDERS -->
|
||||
<section class="section section--tight">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Founders</span>
|
||||
<h2>Three engineers. One full stack.</h2>
|
||||
<p class="muted">
|
||||
Every founder owns a clear technical domain. Together we cover system architecture
|
||||
down to silicon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="dim mono">Loading…</div>
|
||||
<div v-else class="grid grid--3">
|
||||
<FounderCard v-for="f in founders" :key="f.name" :founder="f" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider container" />
|
||||
|
||||
<!-- VALUES -->
|
||||
<section class="section section--tight">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Core values</span>
|
||||
<h2>Fast. Reliable. Robust.</h2>
|
||||
</div>
|
||||
<div class="grid grid--3">
|
||||
<div v-for="v in values" :key="v.name" class="value-card">
|
||||
<h3>{{ v.name }}</h3>
|
||||
<p>{{ v.body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FACTS -->
|
||||
<section class="section section--tight">
|
||||
<div class="container facts">
|
||||
<div>
|
||||
<span class="eyebrow">Where we are</span>
|
||||
<h3 class="facts__h">Surat, Gujarat</h3>
|
||||
<p class="muted">Primary office and engineering team.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="eyebrow">Where we're incubated</span>
|
||||
<h3 class="facts__h">STIIC · IIST</h3>
|
||||
<p class="muted">Space Technology Incubation & Innovation Cell, IIST.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="eyebrow">Stage</span>
|
||||
<h3 class="facts__h">Pre-revenue</h3>
|
||||
<p class="muted">Founded 2026. Three IP cores in customer-ready testing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="section text-center">
|
||||
<div class="container">
|
||||
<h2>Work with us.</h2>
|
||||
<p class="muted">Talk to the founders directly — engineering-first conversations only.</p>
|
||||
<RouterLink to="/contact" class="btn btn--primary">Talk to us</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.story {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.6fr;
|
||||
gap: var(--space-7);
|
||||
}
|
||||
@media (max-width: 860px) { .story { grid-template-columns: 1fr; } }
|
||||
.story__body p { font-size: 1.05rem; }
|
||||
|
||||
.section-head { margin-bottom: var(--space-6); max-width: 720px; }
|
||||
|
||||
.value-card {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
.value-card h3 {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-accent);
|
||||
margin-bottom: var(--space-3);
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-6);
|
||||
}
|
||||
@media (max-width: 860px) { .facts { grid-template-columns: 1fr; } }
|
||||
.facts__h { margin: var(--space-3) 0 var(--space-2); }
|
||||
</style>
|
||||
108
frontend/src/views/BlogPostView.vue
Normal file
108
frontend/src/views/BlogPostView.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
|
||||
const route = useRoute()
|
||||
const post = ref(null)
|
||||
const loading = ref(true)
|
||||
const notFound = ref(false)
|
||||
|
||||
async function load(slug) {
|
||||
loading.value = true
|
||||
notFound.value = false
|
||||
try {
|
||||
post.value = await api.getPost(slug)
|
||||
if (post.value?.title) document.title = `${post.value.title} — RisingCompute`
|
||||
} catch (e) {
|
||||
notFound.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.slug))
|
||||
watch(() => route.params.slug, (s) => s && load(s))
|
||||
|
||||
function fmtDate(iso) {
|
||||
try { return new Date(iso).toLocaleDateString('en-IN', { year: 'numeric', month: 'short', day: 'numeric' }) }
|
||||
catch { return iso }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="container section dim mono">Loading…</div>
|
||||
|
||||
<section v-else-if="notFound" class="section text-center">
|
||||
<div class="container">
|
||||
<h1>Post not found.</h1>
|
||||
<RouterLink to="/blog" class="btn btn--ghost">Back to insights</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<article v-else class="post-page">
|
||||
<header class="post-page__head">
|
||||
<div class="container">
|
||||
<RouterLink to="/blog" class="back-link">← All insights</RouterLink>
|
||||
<span class="pill">{{ post.category_label }}</span>
|
||||
<h1>{{ post.title }}</h1>
|
||||
<p class="dim mono post-page__meta">
|
||||
{{ post.author_name }} · {{ fmtDate(post.published_at) }} · {{ post.read_time_minutes }} min read
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container post-page__body">
|
||||
<p class="post-page__excerpt">{{ post.excerpt }}</p>
|
||||
<!-- Body is markdown-ish; for v1 we render as preformatted text with double-newline paragraphing -->
|
||||
<div class="prose" v-for="(para, idx) in post.body.split(/\n\n+/)" :key="idx">
|
||||
<h2 v-if="para.startsWith('## ')">{{ para.replace(/^##\s*/, '') }}</h2>
|
||||
<p v-else>{{ para }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container post-page__footer">
|
||||
<RouterLink to="/blog" class="btn btn--ghost">← More insights</RouterLink>
|
||||
<RouterLink to="/contact" class="btn btn--primary">Talk to us</RouterLink>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-link { color: var(--color-text-dim); font-size: 0.9rem; }
|
||||
.back-link:hover { color: var(--color-accent); }
|
||||
|
||||
.post-page__head {
|
||||
padding: var(--space-8) 0 var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
background: radial-gradient(800px 360px at 90% 0%, rgba(0, 229, 255, 0.08), transparent 60%);
|
||||
}
|
||||
.post-page__head h1 { margin-top: var(--space-3); max-width: 820px; }
|
||||
.post-page__meta { margin-top: var(--space-2); }
|
||||
|
||||
.post-page__body {
|
||||
max-width: 760px;
|
||||
padding: var(--space-7) var(--space-5);
|
||||
}
|
||||
.post-page__excerpt {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text);
|
||||
border-left: 2px solid var(--color-accent);
|
||||
padding-left: var(--space-4);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.prose h2 { margin-top: var(--space-6); }
|
||||
.prose p { font-size: 1.05rem; }
|
||||
|
||||
.post-page__footer {
|
||||
max-width: 760px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-5);
|
||||
border-top: 1px solid var(--color-border-soft);
|
||||
margin-top: var(--space-6);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
@media (max-width: 540px) { .post-page__footer { flex-direction: column-reverse; } }
|
||||
</style>
|
||||
123
frontend/src/views/BlogView.vue
Normal file
123
frontend/src/views/BlogView.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
import SectionHero from '@/components/SectionHero.vue'
|
||||
|
||||
const posts = ref([])
|
||||
const loading = ref(true)
|
||||
const activeCategory = ref('all')
|
||||
|
||||
onMounted(async () => {
|
||||
try { posts.value = await api.listPosts() }
|
||||
catch (e) {}
|
||||
finally { loading.value = false }
|
||||
})
|
||||
|
||||
const categories = computed(() => {
|
||||
const set = new Map()
|
||||
set.set('all', 'All')
|
||||
for (const p of posts.value) set.set(p.category, p.category_label)
|
||||
return Array.from(set, ([value, label]) => ({ value, label }))
|
||||
})
|
||||
|
||||
const filtered = computed(() =>
|
||||
activeCategory.value === 'all'
|
||||
? posts.value
|
||||
: posts.value.filter(p => p.category === activeCategory.value)
|
||||
)
|
||||
|
||||
function fmtDate(iso) {
|
||||
try { return new Date(iso).toLocaleDateString('en-IN', { year: 'numeric', month: 'short', day: 'numeric' }) }
|
||||
catch { return iso }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionHero
|
||||
eyebrow="Insights"
|
||||
title="Notes from the lab."
|
||||
subtitle="Engineering notes, architecture choices, and the occasional founder essay. Written by the team — not a content desk."
|
||||
/>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="filters" v-if="categories.length > 1">
|
||||
<button
|
||||
v-for="c in categories"
|
||||
:key="c.value"
|
||||
class="filter"
|
||||
:class="{ 'filter--active': activeCategory === c.value }"
|
||||
@click="activeCategory = c.value"
|
||||
>
|
||||
{{ c.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="dim mono">Loading…</div>
|
||||
<div v-else class="grid grid--2 posts">
|
||||
<RouterLink
|
||||
v-for="post in filtered"
|
||||
:key="post.slug"
|
||||
:to="`/blog/${post.slug}`"
|
||||
class="post"
|
||||
>
|
||||
<div class="post__head">
|
||||
<span class="pill">{{ post.category_label }}</span>
|
||||
<span class="dim mono">{{ post.read_time_minutes }} min</span>
|
||||
</div>
|
||||
<h3>{{ post.title }}</h3>
|
||||
<p class="muted">{{ post.excerpt }}</p>
|
||||
<span class="dim mono">{{ post.author_name }} · {{ fmtDate(post.published_at) }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.filter {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
border-radius: 999px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
.filter:hover { border-color: var(--color-accent); color: var(--color-accent); }
|
||||
.filter--active {
|
||||
background: var(--color-accent-soft);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.posts { gap: var(--space-5); }
|
||||
.post {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text);
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
}
|
||||
.post:hover {
|
||||
border-color: rgba(0, 229, 255, 0.4);
|
||||
transform: translateY(-3px);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.post__head { display: flex; justify-content: space-between; align-items: center; }
|
||||
.post h3 { margin: 0; font-size: 1.3rem; }
|
||||
.post p { margin: 0; flex: 1; }
|
||||
</style>
|
||||
141
frontend/src/views/CareersView.vue
Normal file
141
frontend/src/views/CareersView.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
import SectionHero from '@/components/SectionHero.vue'
|
||||
|
||||
const jobs = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try { jobs.value = await api.listJobs() }
|
||||
catch (e) {}
|
||||
finally { loading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionHero
|
||||
eyebrow="Careers"
|
||||
title="Build silicon that flies."
|
||||
subtitle="We're a small founding team in Surat and at IIST. The bar is high; so is the autonomy. If you want to own real RTL, real verification, and real customer relationships from day one, we should talk."
|
||||
/>
|
||||
|
||||
<section class="section section--tight">
|
||||
<div class="container">
|
||||
<div class="why-grid">
|
||||
<div>
|
||||
<h3 class="mono mono-head">Why us</h3>
|
||||
<ul class="reasons">
|
||||
<li>Founder-led engineering. You sit next to the people who decide what to build.</li>
|
||||
<li>Real flight-heritage work — variants of our IP have flown.</li>
|
||||
<li>Ownership over a block, not a sliver.</li>
|
||||
<li>Equity for the first ten people.</li>
|
||||
<li>Surat HQ + IIST presence — both real labs.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mono mono-head">How we hire</h3>
|
||||
<ol class="steps">
|
||||
<li><strong>Intro chat</strong> (30 min) — what you've shipped, what you want next.</li>
|
||||
<li><strong>Technical deep-dive</strong> (90 min) — a real problem from our stack.</li>
|
||||
<li><strong>Founder round</strong> — values, autonomy, expectations.</li>
|
||||
<li><strong>Offer</strong> within a week.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider container" />
|
||||
|
||||
<section class="section section--tight">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Open roles</span>
|
||||
<h2>What we're hiring for.</h2>
|
||||
|
||||
<div v-if="loading" class="dim mono">Loading…</div>
|
||||
<div v-else-if="!jobs.length" class="empty">
|
||||
<p class="muted">
|
||||
No open roles posted right now. We always want to hear from VLSI, RTL,
|
||||
verification, and DSP engineers — send us a note.
|
||||
</p>
|
||||
<RouterLink :to="{ path: '/contact', query: { interest: 'careers' } }" class="btn btn--primary">
|
||||
Introduce yourself
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div v-else class="jobs">
|
||||
<article v-for="j in jobs" :key="j.slug" class="job">
|
||||
<div>
|
||||
<h3>{{ j.title }}</h3>
|
||||
<p class="dim mono job__meta">{{ j.location_label }} · {{ j.employment_type }} · posted {{ j.posted_at }}</p>
|
||||
</div>
|
||||
<RouterLink :to="{ path: '/contact', query: { interest: 'careers' } }" class="btn btn--ghost">
|
||||
Apply →
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.why-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-7);
|
||||
}
|
||||
@media (max-width: 760px) { .why-grid { grid-template-columns: 1fr; } }
|
||||
.mono-head {
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.reasons, .steps {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.reasons { list-style: none; padding: 0; }
|
||||
.reasons li { padding-left: var(--space-5); position: relative; }
|
||||
.reasons li::before {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 600;
|
||||
}
|
||||
.steps { padding-left: var(--space-5); }
|
||||
.steps li::marker { color: var(--color-accent); font-family: var(--font-mono); }
|
||||
|
||||
.jobs {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
.job {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
.job h3 { margin: 0 0 var(--space-1); font-size: 1.15rem; }
|
||||
.job__meta { margin: 0; font-size: 0.8rem; }
|
||||
|
||||
.empty {
|
||||
background: var(--color-surface);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
text-align: center;
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
.empty p { margin: 0 0 var(--space-4); }
|
||||
</style>
|
||||
89
frontend/src/views/ContactView.vue
Normal file
89
frontend/src/views/ContactView.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
import SectionHero from '@/components/SectionHero.vue'
|
||||
import ContactForm from '@/components/ContactForm.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const defaultInterest = String(route.query.interest || 'other')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionHero
|
||||
eyebrow="Talk to us"
|
||||
title="Start an evaluation."
|
||||
subtitle="Conversations land with an engineer, not a sales rep. One business-day reply."
|
||||
/>
|
||||
|
||||
<section class="section">
|
||||
<div class="container contact-grid">
|
||||
<div class="contact-info">
|
||||
<h3>Direct lines</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="dim mono">Email</span>
|
||||
<a href="mailto:contact@risingcompute.in">contact@risingcompute.in</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="dim mono">HQ</span>
|
||||
<p>Surat, Gujarat, India</p>
|
||||
</li>
|
||||
<li>
|
||||
<span class="dim mono">Incubation</span>
|
||||
<p>STIIC · IIST, Thiruvananthapuram</p>
|
||||
</li>
|
||||
<li>
|
||||
<span class="dim mono">Hours</span>
|
||||
<p>Mon–Fri · 09:30–18:30 IST</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<h3>What to expect</h3>
|
||||
<ol class="steps">
|
||||
<li>A short reply from a founder within one business day.</li>
|
||||
<li>A 30-minute qualification call.</li>
|
||||
<li>Datasheet + reference design under NDA.</li>
|
||||
<li>Evaluation licence terms.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ContactForm :default-interest="defaultInterest" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.4fr;
|
||||
gap: var(--space-7);
|
||||
}
|
||||
@media (max-width: 900px) { .contact-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.contact-info ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 var(--space-5);
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.contact-info li { display: grid; gap: var(--space-1); }
|
||||
.contact-info li span.mono {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.contact-info li p { margin: 0; }
|
||||
.contact-info a { font-size: 1.05rem; }
|
||||
|
||||
.steps {
|
||||
padding-left: var(--space-5);
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.steps li::marker { color: var(--color-accent); font-family: var(--font-mono); }
|
||||
</style>
|
||||
314
frontend/src/views/HomeView.vue
Normal file
314
frontend/src/views/HomeView.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
import ProductCard from '@/components/ProductCard.vue'
|
||||
|
||||
const products = ref([])
|
||||
const posts = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [p, b] = await Promise.all([api.listProducts(), api.listPosts()])
|
||||
products.value = p
|
||||
posts.value = b.slice(0, 3)
|
||||
} catch (e) { /* surface a friendlier UI in production */ }
|
||||
finally { loading.value = false }
|
||||
})
|
||||
|
||||
const values = [
|
||||
{ name: 'Fast', body: 'Parallel architectures, deterministic latency, throughput-per-watt that ships.' },
|
||||
{ name: 'Reliable', body: 'Verification-first design. Heritage you can audit, not heritage you have to take on faith.' },
|
||||
{ name: 'Robust', body: 'Built for orbits, factory floors, and defense supply chains — not a controlled bench.' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
<div class="container hero__inner">
|
||||
<div class="hero__copy">
|
||||
<span class="eyebrow">Semiconductor IP · India · Flight-heritage</span>
|
||||
<h1>Accelerating the compute.</h1>
|
||||
<p class="hero__sub">
|
||||
Parallel hardware architectures and IP cores for <strong>AI</strong>, <strong>space</strong>,
|
||||
and <strong>robotics</strong>. Designed in India, flown in orbit.
|
||||
</p>
|
||||
<div class="hero__cta">
|
||||
<RouterLink to="/contact" class="btn btn--primary">Talk to us</RouterLink>
|
||||
<RouterLink to="/technology" class="btn btn--ghost">Read the architecture brief →</RouterLink>
|
||||
</div>
|
||||
<div class="hero__proof">
|
||||
<span class="pill">IIST · STIIC</span>
|
||||
<span class="pill">Flight-heritage IP</span>
|
||||
<span class="pill">Founded 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Decorative compute visual -->
|
||||
<div class="hero__visual" aria-hidden="true">
|
||||
<svg viewBox="0 0 420 420" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="chipGrad" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#00E5FF" stop-opacity="0.55"/>
|
||||
<stop offset="100%" stop-color="#0B1437" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="lineGrad" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#00E5FF" stop-opacity="0"/>
|
||||
<stop offset="50%" stop-color="#00E5FF" stop-opacity="0.7"/>
|
||||
<stop offset="100%" stop-color="#00E5FF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Outer ring -->
|
||||
<circle cx="210" cy="210" r="200" fill="none" stroke="#2A3463" stroke-width="1"/>
|
||||
<circle cx="210" cy="210" r="150" fill="none" stroke="#2A3463" stroke-width="1" stroke-dasharray="2 4"/>
|
||||
<!-- Chip square -->
|
||||
<rect x="130" y="130" width="160" height="160" rx="16" fill="url(#chipGrad)" stroke="#00E5FF" stroke-width="1.5" opacity="0.9"/>
|
||||
<!-- Pins -->
|
||||
<g stroke="#00E5FF" stroke-width="1.5" opacity="0.7">
|
||||
<line x1="50" y1="170" x2="130" y2="170"/>
|
||||
<line x1="50" y1="210" x2="130" y2="210"/>
|
||||
<line x1="50" y1="250" x2="130" y2="250"/>
|
||||
<line x1="290" y1="170" x2="370" y2="170"/>
|
||||
<line x1="290" y1="210" x2="370" y2="210"/>
|
||||
<line x1="290" y1="250" x2="370" y2="250"/>
|
||||
<line x1="170" y1="50" x2="170" y2="130"/>
|
||||
<line x1="210" y1="50" x2="210" y2="130"/>
|
||||
<line x1="250" y1="50" x2="250" y2="130"/>
|
||||
<line x1="170" y1="290" x2="170" y2="370"/>
|
||||
<line x1="210" y1="290" x2="210" y2="370"/>
|
||||
<line x1="250" y1="290" x2="250" y2="370"/>
|
||||
</g>
|
||||
<!-- Inner grid -->
|
||||
<g stroke="#00E5FF" stroke-width="0.6" opacity="0.5">
|
||||
<line x1="170" y1="150" x2="170" y2="270"/>
|
||||
<line x1="210" y1="150" x2="210" y2="270"/>
|
||||
<line x1="250" y1="150" x2="250" y2="270"/>
|
||||
<line x1="150" y1="170" x2="270" y2="170"/>
|
||||
<line x1="150" y1="210" x2="270" y2="210"/>
|
||||
<line x1="150" y1="250" x2="270" y2="250"/>
|
||||
</g>
|
||||
<!-- Animated traveling pulse -->
|
||||
<rect x="50" y="209" width="320" height="2" fill="url(#lineGrad)">
|
||||
<animate attributeName="x" from="-120" to="420" dur="3s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<!-- Core text -->
|
||||
<text x="210" y="216" text-anchor="middle" fill="#F4F6FB" font-family="JetBrains Mono" font-size="14" letter-spacing="2">RC-01</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VALUES -->
|
||||
<section class="section section--tight">
|
||||
<div class="container">
|
||||
<div class="values-grid">
|
||||
<div v-for="v in values" :key="v.name" class="value">
|
||||
<h3>{{ v.name }}.</h3>
|
||||
<p>{{ v.body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PRODUCTS -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">What we ship</span>
|
||||
<h2>One vendor for AI, security, and comms IP.</h2>
|
||||
<p class="muted section-head__lead">
|
||||
Our IP catalogue covers the three blocks every modern payload, robot, and edge product
|
||||
ends up integrating — usually from three different vendors.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="dim mono">Loading products…</div>
|
||||
<div v-else class="grid grid--3">
|
||||
<ProductCard v-for="p in products" :key="p.slug" :product="p" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROOF -->
|
||||
<section class="section proof">
|
||||
<div class="container proof__inner">
|
||||
<div>
|
||||
<span class="eyebrow">Flight heritage</span>
|
||||
<h2>Our IP has flown.<br/>Yours can too.</h2>
|
||||
<p class="muted">
|
||||
The founding team spent two years inside the SSPACE lab at IIST building computation
|
||||
systems for satellites. The IP that flew on those missions is the same IP we're
|
||||
productising for the open market.
|
||||
</p>
|
||||
<RouterLink to="/about" class="btn btn--ghost">Read our story →</RouterLink>
|
||||
</div>
|
||||
<div class="proof__stats">
|
||||
<div><strong>2+</strong><span>years in flight programmes</span></div>
|
||||
<div><strong>3</strong><span>IP cores in customer-ready testing</span></div>
|
||||
<div><strong>5</strong><span>engineers across system + RTL</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- BLOG -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Insights</span>
|
||||
<h2>Notes from the lab.</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="dim mono">Loading…</div>
|
||||
<div v-else class="grid grid--3">
|
||||
<RouterLink
|
||||
v-for="post in posts"
|
||||
:key="post.slug"
|
||||
:to="`/blog/${post.slug}`"
|
||||
class="post-card"
|
||||
>
|
||||
<span class="pill">{{ post.category_label }}</span>
|
||||
<h3>{{ post.title }}</h3>
|
||||
<p class="muted">{{ post.excerpt }}</p>
|
||||
<span class="mono dim">{{ post.author_name }} · {{ post.read_time_minutes }} min</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="section cta">
|
||||
<div class="container cta__inner">
|
||||
<h2>Start an evaluation.</h2>
|
||||
<p class="muted">One business-day reply. Conversations land with an engineer, not a sales rep.</p>
|
||||
<RouterLink to="/contact" class="btn btn--primary">Talk to us</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ---------- Hero ---------- */
|
||||
.hero {
|
||||
padding: var(--space-9) 0 var(--space-8);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero__inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 1fr;
|
||||
gap: var(--space-7);
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.hero__inner { grid-template-columns: 1fr; }
|
||||
.hero__visual { order: -1; max-width: 380px; margin: 0 auto; }
|
||||
}
|
||||
.hero__sub {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-muted);
|
||||
max-width: 580px;
|
||||
margin: var(--space-4) 0 var(--space-6);
|
||||
}
|
||||
.hero__sub strong { color: var(--color-text); font-weight: 600; }
|
||||
.hero__cta {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.hero__proof {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.hero__visual svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 480px;
|
||||
filter: drop-shadow(0 10px 40px rgba(0, 229, 255, 0.15));
|
||||
}
|
||||
|
||||
/* ---------- Values ---------- */
|
||||
.values-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-5);
|
||||
}
|
||||
@media (max-width: 760px) { .values-grid { grid-template-columns: 1fr; } }
|
||||
.value {
|
||||
border-left: 2px solid var(--color-accent);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
}
|
||||
.value h3 { font-size: 1.4rem; margin-bottom: var(--space-2); }
|
||||
.value p { margin: 0; color: var(--color-text-muted); }
|
||||
|
||||
/* ---------- Section heads ---------- */
|
||||
.section-head { margin-bottom: var(--space-6); max-width: 720px; }
|
||||
.section-head h2 { margin-bottom: var(--space-3); }
|
||||
.section-head__lead { font-size: 1.05rem; }
|
||||
|
||||
/* ---------- Proof ---------- */
|
||||
.proof {
|
||||
background: linear-gradient(180deg, transparent, var(--color-bg-soft), transparent);
|
||||
border-top: 1px solid var(--color-border-soft);
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
}
|
||||
.proof__inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: var(--space-7);
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 860px) { .proof__inner { grid-template-columns: 1fr; } }
|
||||
.proof__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.proof__stats > div {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-5) var(--space-4);
|
||||
text-align: center;
|
||||
}
|
||||
.proof__stats strong {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
.proof__stats span { color: var(--color-text-dim); font-size: 0.85rem; }
|
||||
|
||||
/* ---------- Blog tease ---------- */
|
||||
.post-card {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text);
|
||||
transition: border-color var(--transition), transform var(--transition);
|
||||
}
|
||||
.post-card:hover {
|
||||
border-color: rgba(0, 229, 255, 0.4);
|
||||
transform: translateY(-3px);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.post-card h3 { margin: 0; font-size: 1.2rem; }
|
||||
.post-card p { margin: 0; flex: 1; }
|
||||
|
||||
/* ---------- Final CTA ---------- */
|
||||
.cta {
|
||||
text-align: center;
|
||||
background:
|
||||
radial-gradient(600px 240px at 50% 50%, rgba(0, 229, 255, 0.12), transparent 70%);
|
||||
}
|
||||
.cta__inner { max-width: 640px; margin: 0 auto; }
|
||||
.cta h2 { margin-bottom: var(--space-3); }
|
||||
.cta p { margin-bottom: var(--space-5); }
|
||||
</style>
|
||||
25
frontend/src/views/NotFoundView.vue
Normal file
25
frontend/src/views/NotFoundView.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section nf">
|
||||
<div class="container nf__inner">
|
||||
<span class="mono dim">ERR · 404</span>
|
||||
<h1>Lost in orbit.</h1>
|
||||
<p class="muted">The page you're looking for has drifted. Let's get you back.</p>
|
||||
<div class="nf__cta">
|
||||
<RouterLink to="/" class="btn btn--primary">Home</RouterLink>
|
||||
<RouterLink to="/products" class="btn btn--ghost">Products</RouterLink>
|
||||
<RouterLink to="/contact" class="btn btn--ghost">Talk to us</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nf { min-height: 60vh; display: flex; align-items: center; }
|
||||
.nf__inner { text-align: center; max-width: 560px; margin: 0 auto; }
|
||||
.nf h1 { margin-top: var(--space-3); font-size: clamp(2.5rem, 7vw, 4.5rem); }
|
||||
.nf__cta { display: flex; gap: var(--space-3); justify-content: center; flex-wrap: wrap; margin-top: var(--space-5); }
|
||||
</style>
|
||||
231
frontend/src/views/ProductDetailView.vue
Normal file
231
frontend/src/views/ProductDetailView.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
|
||||
const route = useRoute()
|
||||
const product = ref(null)
|
||||
const loading = ref(true)
|
||||
const notFound = ref(false)
|
||||
|
||||
async function load(slug) {
|
||||
loading.value = true
|
||||
notFound.value = false
|
||||
try {
|
||||
product.value = await api.getProduct(slug)
|
||||
if (product.value?.name) document.title = `${product.value.name} — RisingCompute`
|
||||
} catch (e) {
|
||||
notFound.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => load(route.params.slug))
|
||||
watch(() => route.params.slug, (s) => s && load(s))
|
||||
|
||||
const interestMap = {
|
||||
ai: 'ai-ip',
|
||||
security: 'security-ip',
|
||||
comms: 'comms-ip',
|
||||
asic: 'custom-asic',
|
||||
}
|
||||
const contactInterest = computed(() => interestMap[product.value?.category] || 'other')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="container section dim mono">Loading…</div>
|
||||
|
||||
<section v-else-if="notFound" class="section text-center">
|
||||
<div class="container">
|
||||
<h1>Product not found.</h1>
|
||||
<p class="muted">It may have moved, or never existed.</p>
|
||||
<RouterLink to="/products" class="btn btn--ghost">Back to products</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template v-else>
|
||||
<!-- HERO -->
|
||||
<section class="product-hero">
|
||||
<div class="container">
|
||||
<RouterLink to="/products" class="back-link">← All products</RouterLink>
|
||||
<span class="pill">{{ product.category_label }}</span>
|
||||
<h1>{{ product.name }}</h1>
|
||||
<p class="product-hero__tagline">{{ product.tagline }}</p>
|
||||
<div class="product-hero__cta">
|
||||
<RouterLink :to="{ path: '/contact', query: { interest: contactInterest } }" class="btn btn--primary">
|
||||
{{ product.primary_cta_label }}
|
||||
</RouterLink>
|
||||
<RouterLink to="/contact" class="btn btn--ghost">Request datasheet →</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OVERVIEW + BENEFITS -->
|
||||
<section class="section">
|
||||
<div class="container product-grid">
|
||||
<div class="product-grid__copy">
|
||||
<span class="eyebrow">Overview</span>
|
||||
<p class="product-grid__lead">{{ product.summary }}</p>
|
||||
<p>{{ product.description }}</p>
|
||||
</div>
|
||||
|
||||
<aside class="product-grid__side">
|
||||
<h3 class="aside-h">Benefits</h3>
|
||||
<ul class="check-list">
|
||||
<li v-for="b in product.benefits" :key="b">
|
||||
<span class="check">✓</span><span>{{ b }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section class="section section--tight features-section" v-if="product.features?.length">
|
||||
<div class="container">
|
||||
<span class="eyebrow">What's included</span>
|
||||
<h2>Features</h2>
|
||||
<div class="grid grid--3 features">
|
||||
<div v-for="f in product.features" :key="f" class="feature">
|
||||
<span class="feature__dot mono">+</span>
|
||||
<p>{{ f }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SPECS -->
|
||||
<section class="section" v-if="product.spec_table?.length">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Spec sheet</span>
|
||||
<h2>At a glance</h2>
|
||||
<div class="specs">
|
||||
<div v-for="row in product.spec_table" :key="row.label" class="spec-row">
|
||||
<span class="spec-row__label mono">{{ row.label }}</span>
|
||||
<span class="spec-row__value">{{ row.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dim mono spec-note">
|
||||
Full numbers, gate counts, and target-node performance disclosed under NDA.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="section cta">
|
||||
<div class="container cta__inner">
|
||||
<h2>Evaluate {{ product.name }}.</h2>
|
||||
<p class="muted">Get a datasheet, a reference design, and a 30-minute architect call.</p>
|
||||
<RouterLink :to="{ path: '/contact', query: { interest: contactInterest } }" class="btn btn--primary">
|
||||
{{ product.primary_cta_label }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.back-link:hover { color: var(--color-accent); }
|
||||
|
||||
.product-hero {
|
||||
padding: var(--space-8) 0 var(--space-6);
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
background: radial-gradient(800px 360px at 90% 0%, rgba(0, 229, 255, 0.08), transparent 60%);
|
||||
}
|
||||
.product-hero h1 { margin-top: var(--space-3); }
|
||||
.product-hero__tagline { color: var(--color-accent); font-size: 1.2rem; max-width: 720px; }
|
||||
.product-hero__cta { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-5); }
|
||||
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr;
|
||||
gap: var(--space-7);
|
||||
}
|
||||
@media (max-width: 900px) { .product-grid { grid-template-columns: 1fr; } }
|
||||
.product-grid__lead { font-size: 1.15rem; color: var(--color-text); }
|
||||
.product-grid__side {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5);
|
||||
position: sticky;
|
||||
top: calc(var(--nav-height) + var(--space-4));
|
||||
}
|
||||
.aside-h {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-dim);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.check-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.check-list li {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
align-items: flex-start;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.check {
|
||||
color: var(--color-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.features-section { background: linear-gradient(180deg, transparent, var(--color-bg-soft), transparent); border-block: 1px solid var(--color-border-soft); }
|
||||
.feature {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
align-items: flex-start;
|
||||
padding: var(--space-3) 0;
|
||||
}
|
||||
.feature__dot {
|
||||
flex: 0 0 28px;
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.feature p { margin: 0; color: var(--color-text); }
|
||||
|
||||
.specs {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.spec-row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-top: 1px solid var(--color-border-soft);
|
||||
}
|
||||
.spec-row:first-child { border-top: 0; }
|
||||
.spec-row__label { color: var(--color-text-dim); font-size: 0.85rem; letter-spacing: 0.05em; text-transform: uppercase; }
|
||||
.spec-row__value { color: var(--color-text); }
|
||||
@media (max-width: 640px) {
|
||||
.spec-row { grid-template-columns: 1fr; gap: var(--space-1); }
|
||||
}
|
||||
.spec-note { margin-top: var(--space-3); font-size: 0.78rem; }
|
||||
|
||||
.cta {
|
||||
text-align: center;
|
||||
background: radial-gradient(600px 240px at 50% 50%, rgba(0, 229, 255, 0.1), transparent 70%);
|
||||
}
|
||||
.cta__inner { max-width: 640px; margin: 0 auto; }
|
||||
.cta p { margin-bottom: var(--space-5); }
|
||||
</style>
|
||||
53
frontend/src/views/ProductsView.vue
Normal file
53
frontend/src/views/ProductsView.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { api } from '@/api/client'
|
||||
import SectionHero from '@/components/SectionHero.vue'
|
||||
import ProductCard from '@/components/ProductCard.vue'
|
||||
|
||||
const products = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try { products.value = await api.listProducts() }
|
||||
catch (e) {}
|
||||
finally { loading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionHero
|
||||
eyebrow="Products"
|
||||
title="IP cores for AI, space, and robotics."
|
||||
subtitle="Three production-bound blocks. Quote-based licensing. Datasheets and evaluation kits on request."
|
||||
/>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div v-if="loading" class="dim mono">Loading…</div>
|
||||
<div v-else class="grid grid--3">
|
||||
<ProductCard v-for="p in products" :key="p.slug" :product="p" />
|
||||
</div>
|
||||
|
||||
<div class="cta-inline">
|
||||
<p class="muted">Looking for a custom block or full ASIC service?</p>
|
||||
<RouterLink to="/contact" class="btn btn--ghost">Start a conversation →</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cta-inline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-7);
|
||||
padding: var(--space-5);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
@media (max-width: 640px) { .cta-inline { flex-direction: column; align-items: flex-start; } }
|
||||
.cta-inline p { margin: 0; }
|
||||
</style>
|
||||
156
frontend/src/views/TechnologyView.vue
Normal file
156
frontend/src/views/TechnologyView.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import SectionHero from '@/components/SectionHero.vue'
|
||||
|
||||
const pillars = [
|
||||
{
|
||||
title: 'Parallel by design',
|
||||
body: 'Datapaths are parallel from the first line of RTL. The architecture is not a serial CPU that we widened — it is parallel-native.',
|
||||
},
|
||||
{
|
||||
title: 'Verification-first',
|
||||
body: 'Every block ships with UVM testbenches, a coverage plan, and traceable test vectors. Heritage you can audit, not heritage you have to take on faith.',
|
||||
},
|
||||
{
|
||||
title: 'FPGA → ASIC portability',
|
||||
body: 'The same RTL synthesises on Xilinx UltraScale+, Intel Agilex, and standard 28nm / 40nm ASIC libraries. Prototype fast, ship slow, but with the same code.',
|
||||
},
|
||||
{
|
||||
title: 'Security as architecture',
|
||||
body: 'Constant-time datapaths, side-channel-aware routing, and a security white paper per release — not a checklist bolted on at integration time.',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionHero
|
||||
eyebrow="Technology"
|
||||
title="An architecture built for AI, space, and robotics — in that order."
|
||||
subtitle="A short tour of the design principles that show up in every block we ship. If you want the spec-sheet version, request a datasheet."
|
||||
/>
|
||||
|
||||
<!-- PILLARS -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="grid grid--2 pillars">
|
||||
<article v-for="(p, i) in pillars" :key="p.title" class="pillar">
|
||||
<span class="pillar__num mono">{{ String(i + 1).padStart(2, '0') }}</span>
|
||||
<h3>{{ p.title }}</h3>
|
||||
<p class="muted">{{ p.body }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DIAGRAM -->
|
||||
<section class="section diagram-section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">System view</span>
|
||||
<h2>The stack, at a glance.</h2>
|
||||
<p class="muted">Three product blocks. One shared verification toolchain. Same RTL from FPGA to ASIC.</p>
|
||||
</div>
|
||||
|
||||
<div class="stack-diagram">
|
||||
<div class="layer layer--top">
|
||||
<span class="layer__title">Customer SoC / FPGA</span>
|
||||
<span class="layer__sub mono">Your design</span>
|
||||
</div>
|
||||
<div class="layer__pipe"></div>
|
||||
<div class="layer-row">
|
||||
<div class="layer layer--block">
|
||||
<span class="layer__title">AI Inference IP</span>
|
||||
<span class="layer__sub mono">INT8 / INT4</span>
|
||||
</div>
|
||||
<div class="layer layer--block">
|
||||
<span class="layer__title">Cybersecurity IP</span>
|
||||
<span class="layer__sub mono">AES · SHA · ECC</span>
|
||||
</div>
|
||||
<div class="layer layer--block">
|
||||
<span class="layer__title">Communication IP</span>
|
||||
<span class="layer__sub mono">SpaceWire · CAN · UART</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer__pipe"></div>
|
||||
<div class="layer layer--base">
|
||||
<span class="layer__title">Verification & Integration Toolchain</span>
|
||||
<span class="layer__sub mono">UVM · Reference designs · Integration guides</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="section text-center">
|
||||
<div class="container">
|
||||
<h2>Want the long version?</h2>
|
||||
<p class="muted">Request a technical brief or schedule a 30-minute architect call.</p>
|
||||
<RouterLink to="/contact" class="btn btn--primary">Request the brief</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pillars { gap: var(--space-5); }
|
||||
.pillar {
|
||||
background: linear-gradient(180deg, var(--color-surface), var(--color-surface-2));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
position: relative;
|
||||
}
|
||||
.pillar__num {
|
||||
color: var(--color-accent);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.1em;
|
||||
display: block;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.diagram-section {
|
||||
background: linear-gradient(180deg, transparent, var(--color-bg-soft), transparent);
|
||||
border-block: 1px solid var(--color-border-soft);
|
||||
}
|
||||
.section-head { margin-bottom: var(--space-6); max-width: 720px; }
|
||||
|
||||
.stack-diagram {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-2);
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.layer {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
text-align: center;
|
||||
}
|
||||
.layer--top {
|
||||
background: linear-gradient(180deg, rgba(0, 229, 255, 0.08), var(--color-surface));
|
||||
border-color: rgba(0, 229, 255, 0.3);
|
||||
}
|
||||
.layer--base {
|
||||
background: linear-gradient(0deg, rgba(0, 229, 255, 0.06), var(--color-surface));
|
||||
border-color: rgba(0, 229, 255, 0.25);
|
||||
}
|
||||
.layer__title { display: block; font-weight: 600; font-size: 1.05rem; color: var(--color-text); }
|
||||
.layer__sub { display: block; color: var(--color-text-dim); font-size: 0.8rem; margin-top: var(--space-1); letter-spacing: 0.05em; }
|
||||
.layer__pipe {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: var(--color-accent);
|
||||
align-self: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.layer-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.layer-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user