updates
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
+6
-1
@@ -1 +1,6 @@
|
||||
@extends('layouts.app') @section('content')<h1>Licensing administration</h1><div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a>@endsection
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
|
||||
Regular → Executable
Regular → Executable
@@ -0,0 +1,24 @@
|
||||
@extends('layouts.app', ['title' => 'Support Inbox — Rsync Studio'])
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Support inbox</h1><p class="lead compact">Customer, licence, billing and privacy requests.</p></div></section>
|
||||
<form method="get" class="filter-bar">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status" onchange="this.form.submit()">
|
||||
<option value="">All</option>
|
||||
@foreach(['new','open','waiting','resolved','closed'] as $status)<option value="{{ $status }}" @selected(request('status') === $status)>{{ ucfirst($status) }}</option>@endforeach
|
||||
</select>
|
||||
</form>
|
||||
<div class="admin-ticket-list">
|
||||
@forelse($tickets as $ticket)
|
||||
<a class="admin-ticket-row" href="{{ route('admin.support.show', $ticket) }}">
|
||||
<div><span class="ticket-ref">{{ $ticket->reference }}</span><strong>{{ $ticket->subject }}</strong><small>{{ $ticket->name }} · {{ $ticket->email }}</small></div>
|
||||
<span class="ticket-category">{{ ucfirst($ticket->category) }}</span>
|
||||
<span class="ticket-status status-{{ $ticket->status }}">{{ ucfirst($ticket->status) }}</span>
|
||||
<time>{{ $ticket->created_at->diffForHumans() }}</time>
|
||||
</a>
|
||||
@empty
|
||||
<div class="empty-state"><h2>No support requests</h2><p>The support inbox is empty.</p></div>
|
||||
@endforelse
|
||||
</div>
|
||||
{{ $tickets->links() }}
|
||||
@endsection
|
||||
@@ -0,0 +1,14 @@
|
||||
@extends('layouts.app', ['title' => $ticket->reference.' — Support'])
|
||||
@section('content')
|
||||
<a class="back-link" href="{{ route('admin.support.index') }}">← Support inbox</a>
|
||||
<section class="ticket-detail-head"><div><span class="ticket-ref">{{ $ticket->reference }}</span><h1>{{ $ticket->subject }}</h1><p>{{ $ticket->name }} · <a href="mailto:{{ $ticket->email }}?subject=Re:%20{{ rawurlencode($ticket->reference.' '.$ticket->subject) }}">{{ $ticket->email }}</a></p></div><span class="ticket-status status-{{ $ticket->status }}">{{ ucfirst($ticket->status) }}</span></section>
|
||||
<div class="detail-grid">
|
||||
<article class="panel ticket-message"><h2>Request</h2><div class="ticket-meta"><span>{{ ucfirst($ticket->category) }}</span><span>{{ $ticket->created_at->format('j M Y H:i') }}</span></div><div class="message-body">{!! nl2br(e($ticket->message)) !!}</div><a class="btn btn-primary" href="mailto:{{ $ticket->email }}?subject=Re:%20{{ rawurlencode($ticket->reference.' '.$ticket->subject) }}">Reply by email</a></article>
|
||||
<aside class="panel"><h2>Manage</h2><form method="post" action="{{ route('admin.support.update', $ticket) }}">@csrf @method('PATCH')
|
||||
<label>Status<select name="status">@foreach(['new','open','waiting','resolved','closed'] as $status)<option value="{{ $status }}" @selected($ticket->status === $status)>{{ ucfirst($status) }}</option>@endforeach</select></label>
|
||||
<label>Priority<select name="priority">@foreach(['low','normal','high','urgent'] as $priority)<option value="{{ $priority }}" @selected($ticket->priority === $priority)>{{ ucfirst($priority) }}</option>@endforeach</select></label>
|
||||
<label>Assigned to<select name="assigned_to"><option value="">Unassigned</option>@foreach($admins as $admin)<option value="{{ $admin->id }}" @selected($ticket->assigned_to === $admin->id)>{{ $admin->name }} — {{ $admin->email }}</option>@endforeach</select></label>
|
||||
<button class="full-width">Save changes</button>
|
||||
</form></aside>
|
||||
</div>
|
||||
@endsection
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1,91 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<div class="hero-card">
|
||||
<span class="eyebrow">Rsync Studio</span>
|
||||
<h1>Choose your licence</h1>
|
||||
<p class="lead">Every subscription starts with a 7-day free trial. Cancel before the trial ends and you won't be charged.</p>
|
||||
@if($paddleEnvironment === 'sandbox')<div class="sandbox-badge">Sandbox checkout — no real payment is taken</div>@endif
|
||||
</div>
|
||||
|
||||
<div class="pricing-grid">
|
||||
@foreach($plans as $code => $plan)
|
||||
<article class="price-card {{ str_starts_with($code, 'professional') ? 'featured' : '' }}">
|
||||
<div>
|
||||
<div class="plan-heading">
|
||||
<div>
|
||||
<h2>{{ $plan['name'] }}</h2>
|
||||
<div class="muted">{{ $plan['billing'] }} billing</div>
|
||||
</div>
|
||||
@if($plan['billing'] === 'Annual')<span class="pill">Best value</span>@endif
|
||||
</div>
|
||||
<div class="price" data-price-display="{{ $plan['price_id'] }}" data-billing="{{ strtolower($plan['billing']) }}">{{ $plan['display_price'] }}</div>
|
||||
<ul class="features">
|
||||
<li>7-day free trial</li>
|
||||
<li>{{ $plan['activation_limit'] }} active Mac{{ $plan['activation_limit'] === 1 ? '' : 's' }}</li>
|
||||
<li>Secure online activation</li>
|
||||
<li>Licence management portal</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="checkout-button" type="button" data-price-id="{{ $plan['price_id'] }}" data-plan="{{ $plan['name'] }} {{ $plan['billing'] }}">Start free trial</button>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="card checkout-status" id="checkout-status" hidden>
|
||||
<h2>Purchase received</h2>
|
||||
<p>Paddle is confirming your subscription. Your Rsync Studio licence key will be sent to the email used at checkout. You can then activate it in <strong>Rsync Studio → Settings → Licence</strong>.</p>
|
||||
<p class="muted">If the email takes a moment, wait for the Paddle confirmation and check your junk folder. You can also create a portal account with the same email address.</p>
|
||||
<a class="btn" href="{{ route('register') }}">Create portal account</a>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||
<script>
|
||||
(() => {
|
||||
const token = @json($paddleToken);
|
||||
const environment = @json($paddleEnvironment);
|
||||
if (environment === 'sandbox') Paddle.Environment.set('sandbox');
|
||||
Paddle.Initialize({
|
||||
token,
|
||||
eventCallback(event) {
|
||||
if (event && event.name === 'checkout.completed') {
|
||||
document.getElementById('checkout-status').hidden = false;
|
||||
document.getElementById('checkout-status').scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const priceElements = new Map(
|
||||
Array.from(document.querySelectorAll('[data-price-display]')).map(element => [element.dataset.priceDisplay, element])
|
||||
);
|
||||
const previewItems = Array.from(priceElements.keys()).map(priceId => ({priceId, quantity: 1}));
|
||||
Paddle.PricePreview({items: previewItems}).then(result => {
|
||||
for (const item of (result?.data?.details?.lineItems ?? [])) {
|
||||
const element = priceElements.get(item?.price?.id);
|
||||
const formatted = item?.formattedTotals?.subtotal ?? item?.formattedUnitTotals?.subtotal;
|
||||
if (!element || !formatted) continue;
|
||||
const suffix = element.dataset.billing === 'annual' ? ' / year' : ' / month';
|
||||
element.textContent = formatted + suffix;
|
||||
}
|
||||
}).catch(() => {
|
||||
// Keep the server-rendered GBP fallback if Paddle pricing preview is unavailable.
|
||||
});
|
||||
|
||||
document.querySelectorAll('.checkout-button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
Paddle.Checkout.open({
|
||||
items: [{priceId: button.dataset.priceId, quantity: 1}],
|
||||
settings: {
|
||||
displayMode: 'overlay',
|
||||
theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
||||
allowLogout: false
|
||||
},
|
||||
customData: {
|
||||
source: 'rsync-studio-buy-page',
|
||||
plan: button.dataset.plan
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
Regular → Executable
+315
-1
@@ -1 +1,315 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><title>{{ $title ?? 'Rsync Studio' }}</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#f5f6f8;color:#18202a}nav{background:#111827;color:#fff;padding:14px 24px;display:flex;gap:18px;align-items:center}nav a{color:#fff;text-decoration:none}.wrap{max-width:980px;margin:32px auto;padding:0 20px}.card{background:#fff;border:1px solid #d9dee7;border-radius:12px;padding:22px;margin:16px 0;box-shadow:0 2px 8px #0000000d}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px}.muted{color:#667085}.ok{color:#067647}.bad{color:#b42318}button,.btn{display:inline-block;border:0;border-radius:8px;padding:10px 14px;background:#2563eb;color:#fff;text-decoration:none;cursor:pointer}button.danger{background:#b42318}input{width:100%;box-sizing:border-box;padding:10px;border:1px solid #cbd5e1;border-radius:8px;margin:6px 0 14px}label{font-weight:600}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px;border-bottom:1px solid #e5e7eb}.alert{padding:12px;border-radius:8px;background:#ecfdf3;margin-bottom:14px}.error{padding:12px;border-radius:8px;background:#fef3f2;margin-bottom:14px}</style></head><body><nav><strong>Rsync Studio</strong><a href="{{ route('home') }}">Home</a>@auth @if(auth()->user()->hasVerifiedEmail())<a href="{{ route('portal.index') }}">My licences</a>@endif @if(auth()->user()->is_admin)<a href="{{ route('admin.index') }}">Admin</a>@endif <span style="margin-left:auto">{{ auth()->user()->email }}</span><form method="post" action="{{ route('logout') }}">@csrf<button>Sign out</button></form>@else<span style="margin-left:auto"></span><a href="{{ route('login') }}">Sign in</a><a href="{{ route('register') }}">Create account</a>@endauth</nav><main class="wrap">@if(session('status'))<div class="alert">{{ session('status') }}</div>@endif @if($errors->any())<div class="error">{{ $errors->first() }}</div>@endif @yield('content')</main></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
@php
|
||||
$pageTitle = $title ?? 'Rsync Studio';
|
||||
$pageDescription = $description ?? 'Native rsync job management for macOS. Create, schedule, watch and review reliable local and SSH sync jobs.';
|
||||
$canonicalUrl = url()->current();
|
||||
$socialImage = url('/icons/og-rsync-studio.png');
|
||||
@endphp
|
||||
|
||||
<title>{{ $pageTitle }}</title>
|
||||
<meta name="description" content="{{ $pageDescription }}">
|
||||
<meta name="theme-color" content="#0b1220">
|
||||
<meta name="application-name" content="Rsync Studio">
|
||||
<meta name="apple-mobile-web-app-title" content="Rsync Studio">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="msapplication-TileColor" content="#2563eb">
|
||||
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">
|
||||
@if(app()->environment('production'))
|
||||
<meta name="robots" content="index,follow,max-image-preview:large">
|
||||
@else
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
@endif
|
||||
|
||||
<link rel="canonical" href="{{ $canonicalUrl }}">
|
||||
<link rel="icon" href="{{ asset('favicon.ico') }}" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('icons/favicon-32x32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('icons/favicon-16x16.png') }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('icons/apple-touch-icon.png') }}">
|
||||
<link rel="mask-icon" href="{{ asset('icons/safari-pinned-tab.svg') }}" color="#2563eb">
|
||||
<link rel="manifest" href="{{ asset('site.webmanifest') }}">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Rsync Studio">
|
||||
<meta property="og:title" content="{{ $pageTitle }}">
|
||||
<meta property="og:description" content="{{ $pageDescription }}">
|
||||
<meta property="og:url" content="{{ $canonicalUrl }}">
|
||||
<meta property="og:image" content="{{ $socialImage }}">
|
||||
<meta property="og:image:width" content="512">
|
||||
<meta property="og:image:height" content="512">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="{{ $pageTitle }}">
|
||||
<meta name="twitter:description" content="{{ $pageDescription }}">
|
||||
<meta name="twitter:image" content="{{ $socialImage }}">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
color-scheme:light;
|
||||
--bg:#f7f9fc;--surface:#fff;--surface-soft:#f8fafc;--text:#172033;--muted:#667085;
|
||||
--line:#e4e7ec;--line-strong:#d0d5dd;--blue:#2563eb;--blue-dark:#1d4ed8;--blue-soft:#eff6ff;
|
||||
--green:#067647;--green-soft:#ecfdf3;--red:#b42318;--red-soft:#fef3f2;--nav:#0b1220;
|
||||
--shadow-sm:0 1px 3px rgba(16,24,40,.06);--shadow-md:0 14px 36px rgba(15,23,42,.08);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",sans-serif;margin:0;background:var(--bg);color:var(--text);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
a{color:inherit}
|
||||
img{max-width:100%;display:block}
|
||||
button,input{font:inherit}
|
||||
:focus-visible{outline:3px solid rgba(37,99,235,.28);outline-offset:3px}
|
||||
|
||||
.site-header{position:sticky;top:0;z-index:50;background:rgba(11,18,32,.94);backdrop-filter:saturate(180%) blur(18px);border-bottom:1px solid rgba(255,255,255,.08)}
|
||||
.nav-shell{max-width:1220px;margin:0 auto;min-height:68px;padding:0 24px;display:flex;align-items:center;gap:28px}
|
||||
.brand{display:inline-flex;align-items:center;gap:10px;color:#fff;text-decoration:none;font-weight:800;letter-spacing:-.01em;white-space:nowrap}
|
||||
.brand img{width:34px;height:34px;border-radius:9px;box-shadow:0 5px 14px rgba(37,99,235,.25)}
|
||||
.nav-links{display:flex;align-items:center;gap:22px}
|
||||
.nav-links a{color:#cbd5e1;text-decoration:none;font-weight:650;font-size:14px;transition:color .16s ease}
|
||||
.nav-links a:hover{color:#fff}.nav-alert-count{display:inline-flex;min-width:18px;height:18px;align-items:center;justify-content:center;margin-left:5px;padding:0 5px;border-radius:99px;background:#ef4444;color:#fff;font-size:10px;font-weight:900;vertical-align:1px}
|
||||
.nav-account{margin-left:auto;display:flex;align-items:center;gap:12px;min-width:0}
|
||||
.nav-email{color:#cbd5e1;font-size:13px;max-width:230px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.nav-auth-link{color:#e2e8f0;text-decoration:none;font-weight:650;font-size:14px}
|
||||
.nav-cta{display:inline-flex;align-items:center;justify-content:center;padding:9px 13px;border-radius:9px;background:#fff;color:#0f172a!important;text-decoration:none!important;font-weight:800!important}
|
||||
.nav-signout{background:transparent;border:1px solid rgba(255,255,255,.18);color:#e2e8f0;padding:8px 11px;border-radius:9px;font-weight:700;cursor:pointer}
|
||||
.nav-signout:hover{background:rgba(255,255,255,.08)}
|
||||
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:42px 24px 72px;min-height:calc(100vh - 220px)}
|
||||
.wrap-wide{max-width:1220px}
|
||||
.hero-card{padding:28px 0 18px}
|
||||
.hero-card h1,.portal-header h1,.licence-detail-head h1{font-size:38px;line-height:1.08;margin:7px 0 10px;letter-spacing:-.035em}
|
||||
.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:850;color:var(--blue)}
|
||||
.lead{font-size:17px;line-height:1.62;max-width:720px;color:#475467}.lead.compact{margin:0}
|
||||
|
||||
.btn,button{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:10px;padding:10px 15px;background:var(--blue);color:#fff;text-decoration:none;cursor:pointer;font:inherit;font-weight:750;line-height:1.2;transition:transform .15s ease,background .15s ease,box-shadow .15s ease}
|
||||
.btn:hover,button:hover{background:var(--blue-dark)}
|
||||
.btn-primary{box-shadow:0 7px 18px rgba(37,99,235,.22)}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 9px 22px rgba(37,99,235,.28)}
|
||||
.btn-secondary,button.btn-secondary{background:#fff;color:#344054;border:1px solid #d0d5dd;box-shadow:none}
|
||||
.btn-secondary:hover,button.btn-secondary:hover{background:#f9fafb}
|
||||
.btn-dark{background:#111827}.btn-dark:hover{background:#030712}
|
||||
.danger{background:var(--red)}.danger:hover{background:#912018}
|
||||
.danger-outline{background:#fff;color:var(--red);border:1px solid #f3b7b2}.danger-outline:hover{background:var(--red-soft)}
|
||||
.full-width{width:100%}
|
||||
|
||||
.pricing-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin:20px 0 28px}
|
||||
.price-card,.card,.panel{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:24px;box-shadow:var(--shadow-sm)}
|
||||
.price-card{display:flex;flex-direction:column;justify-content:space-between;gap:24px}.price-card.featured{border-color:#9dbcf7}
|
||||
.plan-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.plan-heading h2{margin:0 0 5px}.price{font-size:28px;font-weight:850;margin:20px 0}.features{padding-left:20px;line-height:1.9}
|
||||
.pill,.sandbox-badge{display:inline-block;border-radius:999px;padding:5px 9px;font-size:12px;font-weight:750;background:var(--blue-soft);color:var(--blue-dark)}
|
||||
.sandbox-badge{margin-top:8px;background:#fff7ed;color:#9a3412}.checkout-button{width:100%;font-size:15px;font-weight:750;padding:12px 16px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px}.muted{color:var(--muted)}.ok{color:var(--green)}.bad{color:var(--red)}
|
||||
input{width:100%;padding:11px 12px;border:1px solid #cbd5e1;border-radius:9px;margin:6px 0 14px;background:#fff;color:var(--text)}
|
||||
label{font-weight:650}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px;border-bottom:1px solid #e5e7eb}
|
||||
.alert,.error{padding:13px 15px;border-radius:10px;margin:0 0 20px;font-weight:650}.alert{background:var(--green-soft);color:var(--green);border:1px solid #abefc6}.error{background:var(--red-soft);color:var(--red);border:1px solid #fecdca}
|
||||
|
||||
/* Product launch page */
|
||||
.launch-main{overflow:hidden}
|
||||
.launch-hero{position:relative;background:linear-gradient(180deg,#0b1220 0,#101b31 78%,#f7f9fc 78%);padding:82px 24px 106px;color:#fff}
|
||||
.launch-hero:before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 75% 12%,rgba(59,130,246,.26),transparent 34%),radial-gradient(circle at 18% 32%,rgba(14,165,233,.13),transparent 28%);pointer-events:none}
|
||||
.launch-shell{position:relative;max-width:1220px;margin:0 auto}
|
||||
.hero-grid{display:grid;grid-template-columns:minmax(0,.95fr) minmax(500px,1.05fr);gap:72px;align-items:center}
|
||||
.launch-kicker{display:inline-flex;align-items:center;gap:8px;background:rgba(59,130,246,.12);border:1px solid rgba(147,197,253,.22);color:#bfdbfe;padding:7px 11px;border-radius:999px;font-size:12px;font-weight:800;letter-spacing:.02em}
|
||||
.launch-kicker:before{content:"";width:7px;height:7px;border-radius:50%;background:#60a5fa;box-shadow:0 0 0 4px rgba(96,165,250,.12)}
|
||||
.launch-hero h1{font-size:clamp(46px,6vw,74px);line-height:.98;letter-spacing:-.055em;margin:20px 0 22px;max-width:760px}
|
||||
.hero-copy{font-size:19px;line-height:1.65;color:#cbd5e1;max-width:650px;margin:0}
|
||||
.hero-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:30px}.hero-actions .btn{padding:13px 18px;border-radius:11px}
|
||||
.hero-actions .btn-secondary{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.18);color:#fff}.hero-actions .btn-secondary:hover{background:rgba(255,255,255,.12)}
|
||||
.hero-note{margin:16px 0 0;color:#94a3b8;font-size:13px}
|
||||
.trust-row{display:flex;gap:20px;flex-wrap:wrap;margin-top:34px;color:#cbd5e1;font-size:13px}.trust-item{display:flex;align-items:center;gap:7px}.trust-check{display:inline-grid;place-items:center;width:18px;height:18px;border-radius:50%;background:rgba(34,197,94,.12);color:#86efac;font-size:11px;font-weight:900}
|
||||
|
||||
.app-window{border:1px solid rgba(255,255,255,.15);border-radius:18px;background:#f8fafc;box-shadow:0 34px 70px rgba(0,0,0,.32);overflow:hidden;color:#111827;transform:perspective(1200px) rotateY(-2deg) rotateX(1deg);transform-origin:center}
|
||||
.window-bar{height:46px;background:#eef2f7;border-bottom:1px solid #dde3ec;display:flex;align-items:center;padding:0 14px;gap:8px}.dot{width:11px;height:11px;border-radius:50%}.dot.red{background:#ff5f57}.dot.amber{background:#febc2e}.dot.green{background:#28c840}.window-title{margin-left:8px;font-size:12px;font-weight:750;color:#667085}
|
||||
.app-body{display:grid;grid-template-columns:160px 1fr;min-height:390px}.app-sidebar{background:#111827;color:#cbd5e1;padding:18px 12px}.mini-brand{display:flex;align-items:center;gap:8px;color:#fff;font-weight:800;font-size:12px;margin:0 8px 18px}.mini-brand img{width:26px;height:26px;border-radius:7px}.side-item{padding:8px 10px;border-radius:8px;font-size:11px;margin:3px 0}.side-item.active{background:#263244;color:#fff}.app-content{padding:20px;background:#f8fafc}.app-toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.app-toolbar strong{font-size:15px}.sync-all{font-size:10px;background:#2563eb;color:#fff;padding:7px 9px;border-radius:7px;font-weight:800}.mock-job{background:#fff;border:1px solid #e2e8f0;border-radius:11px;padding:13px 14px;margin-bottom:10px;box-shadow:0 1px 2px rgba(15,23,42,.04)}.mock-job-top{display:flex;align-items:center;justify-content:space-between;gap:12px}.mock-job strong{font-size:12px}.job-state{font-size:9px;font-weight:850;padding:4px 7px;border-radius:999px}.state-ready{background:#ecfdf3;color:#047857}.state-scheduled{background:#eff6ff;color:#1d4ed8}.state-watching{background:#f5f3ff;color:#6d28d9}.mock-job p{font-size:9px;color:#64748b;margin:4px 0 9px}.mock-progress{height:5px;background:#eef2f7;border-radius:999px;overflow:hidden}.mock-progress span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#2563eb,#38bdf8)}.app-statusbar{display:flex;justify-content:space-between;color:#64748b;font-size:9px;margin-top:16px;padding-top:13px;border-top:1px solid #e2e8f0}
|
||||
|
||||
.launch-section{padding:92px 24px}.section-shell{max-width:1120px;margin:0 auto}.section-head{max-width:690px;margin-bottom:42px}.section-head.center{text-align:center;margin-left:auto;margin-right:auto}.section-head h2{font-size:clamp(32px,4vw,46px);line-height:1.08;letter-spacing:-.04em;margin:8px 0 14px}.section-head p{font-size:17px;color:#667085;margin:0;line-height:1.65}
|
||||
.feature-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.feature-card{background:#fff;border:1px solid var(--line);border-radius:18px;padding:26px;box-shadow:var(--shadow-sm)}.feature-icon{display:grid;place-items:center;width:42px;height:42px;border-radius:11px;background:var(--blue-soft);color:var(--blue);font-weight:900;font-size:18px;margin-bottom:18px}.feature-card h3{font-size:18px;margin:0 0 8px;letter-spacing:-.015em}.feature-card p{color:#667085;margin:0;line-height:1.6}
|
||||
.workflow-section{background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.workflow-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.workflow-step{position:relative;padding:4px 10px 4px 0}.step-number{display:grid;place-items:center;width:36px;height:36px;border-radius:10px;background:#101828;color:#fff;font-weight:850;margin-bottom:16px}.workflow-step h3{font-size:17px;margin:0 0 7px}.workflow-step p{color:#667085;margin:0;font-size:14px;line-height:1.6}
|
||||
.security-panel{background:#0f172a;color:#fff;border-radius:24px;padding:48px;display:grid;grid-template-columns:.9fr 1.1fr;gap:60px;align-items:center;box-shadow:var(--shadow-md)}.security-panel h2{font-size:38px;line-height:1.08;letter-spacing:-.04em;margin:8px 0 15px}.security-panel p{color:#cbd5e1;font-size:16px;line-height:1.65}.security-list{display:grid;gap:13px}.security-item{display:flex;gap:12px;align-items:flex-start;background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.08);border-radius:13px;padding:14px}.security-icon{display:grid;place-items:center;width:30px;height:30px;flex:0 0 30px;border-radius:8px;background:rgba(59,130,246,.17);color:#93c5fd;font-weight:900}.security-item strong{display:block;font-size:14px;margin-bottom:2px}.security-item span{display:block;color:#94a3b8;font-size:12px;line-height:1.5}
|
||||
.plan-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;max-width:860px;margin:0 auto}.launch-plan{background:#fff;border:1px solid var(--line);border-radius:20px;padding:30px;box-shadow:var(--shadow-sm);position:relative}.launch-plan.featured{border-color:#9dbcf7;box-shadow:0 18px 44px rgba(37,99,235,.09)}.launch-plan h3{font-size:23px;margin:0}.launch-plan p{color:#667085}.plan-macs{font-size:34px;font-weight:850;letter-spacing:-.04em;margin:24px 0 3px}.plan-sub{font-size:13px;color:#667085}.plan-list{list-style:none;padding:0;margin:24px 0;display:grid;gap:11px}.plan-list li{display:flex;gap:9px;color:#475467}.plan-list li:before{content:"✓";color:#16a34a;font-weight:900}.launch-plan .btn{width:100%;padding:12px}.best-badge{position:absolute;right:18px;top:18px;background:var(--blue-soft);color:var(--blue-dark);border-radius:999px;padding:5px 9px;font-size:11px;font-weight:850}
|
||||
.launch-cta{padding:30px 24px 100px}.cta-box{max-width:1120px;margin:0 auto;background:linear-gradient(135deg,#2563eb,#1d4ed8 55%,#0f4fc9);color:#fff;border-radius:26px;padding:54px;display:flex;align-items:center;justify-content:space-between;gap:36px;box-shadow:0 24px 54px rgba(37,99,235,.2)}.cta-box h2{font-size:38px;line-height:1.08;letter-spacing:-.04em;margin:0 0 10px}.cta-box p{margin:0;color:#dbeafe;font-size:16px}.cta-box .btn{background:#fff;color:#1e3a8a;padding:13px 18px;white-space:nowrap}.cta-box .btn:hover{background:#eff6ff}
|
||||
|
||||
.portal-header{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:26px}.licence-list{display:grid;gap:16px}.licence-card{background:#fff;border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow-sm);overflow:hidden}.licence-card-featured{border-color:#c6d7fb}.licence-main{padding:24px 26px}.licence-title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}.licence-title-row h2{font-size:22px;margin:10px 0 2px;letter-spacing:-.015em}.licence-key{margin:0}.badge-row{display:flex;gap:7px;flex-wrap:wrap}.status-badge,.plan-badge,.billing-badge{display:inline-flex;align-items:center;border-radius:999px;padding:4px 9px;font-size:12px;font-weight:800}.status-active{background:var(--green-soft);color:var(--green)}.status-inactive{background:var(--red-soft);color:var(--red)}.plan-badge{background:var(--blue-soft);color:var(--blue-dark)}.billing-badge{background:#f2f4f7;color:#475467}.licence-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:24px 0 12px;padding-top:20px;border-top:1px solid var(--line)}.licence-stats>div{display:flex;flex-direction:column;gap:3px}.stat-label{font-size:12px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.04em}.licence-stats strong{font-size:15px}.activation-meter{height:7px;background:#eef2f6;border-radius:99px;overflow:hidden}.activation-meter span{display:block;height:100%;background:var(--blue);border-radius:99px}.upgrade-strip,.subscription-warning{margin-top:20px;border-radius:12px;padding:15px 16px;display:flex;align-items:center;justify-content:space-between;gap:16px}.upgrade-strip{background:#f5f8ff;border:1px solid #dbe7ff}.upgrade-strip div{display:flex;flex-direction:column;gap:2px}.upgrade-strip span{color:#475467;font-size:14px}.subscription-warning{background:#fff7ed;border:1px solid #fed7aa}.subscription-warning p{margin:3px 0 0;color:#9a3412;font-size:13px;max-width:720px}.empty-state{background:#fff;border:1px dashed #cbd5e1;border-radius:16px;padding:44px;text-align:center}.empty-state h2{margin-top:0}.history-panel{margin-top:26px;background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden}.history-panel summary{cursor:pointer;padding:16px 20px;font-weight:800}.history-panel summary span{display:inline-flex;margin-left:6px;min-width:22px;height:22px;align-items:center;justify-content:center;border-radius:99px;background:#f2f4f7;color:#475467;font-size:12px}.history-list{border-top:1px solid var(--line)}.history-row{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:18px;padding:14px 20px;border-bottom:1px solid var(--line)}.history-row:last-child{border-bottom:0}.history-row>div:first-child{display:flex;flex-direction:column}.history-row span{font-size:13px;color:var(--muted)}.history-status{font-size:13px;color:var(--muted)}.history-row a{font-weight:700;color:var(--blue);text-decoration:none}.back-link{display:inline-block;margin-bottom:20px;color:#475467;text-decoration:none;font-weight:700}.licence-detail-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:24px}.licence-detail-head h1{font-size:32px}.detail-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(280px,.7fr);gap:18px}.panel{margin:0}.panel h2{margin:0;font-size:20px}.panel-heading{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;padding-bottom:18px;border-bottom:1px solid var(--line)}.panel-heading p{margin:4px 0 0;color:var(--muted)}.activation-count{font-weight:800;background:var(--blue-soft);color:var(--blue-dark);padding:6px 10px;border-radius:999px}.device-row{display:grid;grid-template-columns:42px 1fr auto;gap:14px;align-items:center;padding:17px 0;border-bottom:1px solid var(--line)}.device-row:last-child{border-bottom:0}.device-icon{width:42px;height:42px;display:flex;align-items:center;justify-content:center;background:#f2f4f7;border-radius:10px;font-size:20px}.device-copy{display:flex;flex-direction:column;min-width:0}.device-copy span,.device-copy small{color:var(--muted)}.device-copy small{margin-top:2px}.empty-inline{padding:26px 0 4px;color:var(--muted)}.subscription-panel form+form{margin-top:10px}.subscription-facts{margin:18px 0 22px}.subscription-facts>div{display:flex;justify-content:space-between;gap:16px;padding:11px 0;border-bottom:1px solid var(--line)}.subscription-facts dt{color:var(--muted)}.subscription-facts dd{margin:0;font-weight:700;text-align:right}
|
||||
|
||||
.site-footer{background:#0b1220;color:#94a3b8;border-top:1px solid rgba(255,255,255,.06)}.footer-shell{max-width:1220px;margin:0 auto;padding:34px 24px;display:flex;align-items:center;justify-content:space-between;gap:28px}.footer-brand{display:flex;align-items:center;gap:10px;color:#e2e8f0;font-weight:800}.footer-brand img{width:28px;height:28px;border-radius:7px}.footer-links{display:flex;gap:20px;flex-wrap:wrap}.footer-links a{color:#cbd5e1;text-decoration:none;font-size:13px;font-weight:650}.footer-links a:hover{color:#fff}.footer-cookie-link{background:none!important;border:0!important;padding:0!important;color:#cbd5e1!important;font-size:13px!important;font-weight:650!important;box-shadow:none!important}.footer-cookie-link:hover{color:#fff!important}.footer-copy{font-size:12px;margin-top:5px}
|
||||
|
||||
@media(max-width:980px){
|
||||
.hero-grid{grid-template-columns:1fr;gap:48px}.app-window{max-width:720px;transform:none}.feature-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.workflow-grid{grid-template-columns:repeat(2,1fr)}.security-panel{grid-template-columns:1fr;gap:28px}.nav-email{display:none}
|
||||
}
|
||||
@media(max-width:760px){
|
||||
.site-header{position:relative}.nav-shell{padding:10px 16px;min-height:64px;gap:12px;flex-wrap:wrap}.brand{margin-right:auto}.nav-links{order:3;width:100%;gap:18px;overflow-x:auto;padding:2px 0 4px}.nav-links::-webkit-scrollbar{display:none}.nav-account{margin-left:0}.nav-account .nav-auth-link{display:none}.wrap{padding:28px 16px 48px}.pricing-grid,.plan-grid{grid-template-columns:1fr}.hero-card h1,.portal-header h1{font-size:32px}.portal-header,.licence-detail-head,.licence-title-row,.upgrade-strip,.subscription-warning{align-items:stretch;flex-direction:column}.portal-header .btn,.licence-title-row .btn{width:100%}.licence-stats{grid-template-columns:1fr}.detail-grid{grid-template-columns:1fr}.history-row{grid-template-columns:1fr auto}.history-row a{grid-column:1/-1}.device-row{grid-template-columns:38px 1fr}.device-row form{grid-column:1/-1}.device-row form button{width:100%}
|
||||
.launch-hero{padding:58px 18px 88px;background:linear-gradient(180deg,#0b1220 0,#101b31 84%,#f7f9fc 84%)}.launch-hero h1{font-size:48px}.hero-copy{font-size:17px}.app-body{grid-template-columns:120px 1fr;min-height:330px}.app-sidebar{padding:12px 8px}.side-item{padding:7px 8px}.app-content{padding:14px}.launch-section{padding:68px 18px}.feature-grid,.workflow-grid{grid-template-columns:1fr}.security-panel{padding:30px 24px;border-radius:20px}.security-panel h2{font-size:32px}.cta-box{padding:36px 26px;align-items:flex-start;flex-direction:column}.cta-box h2{font-size:32px}.footer-shell{align-items:flex-start;flex-direction:column}.launch-cta{padding:16px 18px 72px}
|
||||
}
|
||||
@media(max-width:480px){.launch-hero h1{font-size:42px}.hero-actions{flex-direction:column}.hero-actions .btn{width:100%}.app-body{grid-template-columns:1fr}.app-sidebar{display:none}.window-title{display:none}.launch-kicker{font-size:11px}.feature-card{padding:22px}}
|
||||
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
|
||||
|
||||
|
||||
/* Launch polish + GDPR/support UI */
|
||||
.launch-hero{background:linear-gradient(135deg,#07111f 0%,#0b1d36 54%,#0c2b55 100%);padding:78px 24px 82px}
|
||||
.launch-hero:after{content:"";position:absolute;right:-10%;bottom:-36%;width:58%;height:80%;background:radial-gradient(ellipse at center,rgba(37,99,235,.45),rgba(14,165,233,.14) 42%,transparent 68%);filter:blur(18px);pointer-events:none}
|
||||
.app-window{background:#1c1c1e;border-color:rgba(255,255,255,.14);box-shadow:0 34px 70px rgba(0,0,0,.42)}
|
||||
.window-bar{background:#252527;border-bottom-color:#353539;color:#d4d4d8}.app-body{background:#1c1c1e}.app-sidebar{background:#252526;border-right:1px solid #39393d}.mini-brand{color:#f4f4f5}.side-item{color:#b7b7bd}.side-item.active{background:#1677ea;color:white}.app-content{background:#1c1c1e;color:#f4f4f5}.mock-job{background:#242426;border-color:#37373b;box-shadow:none}.mock-job p,.app-statusbar{color:#9f9fa6}.app-toolbar{color:#f4f4f5}.mock-progress{background:#34343a}
|
||||
.launch-section{position:relative}.feature-card{border:1px solid rgba(148,163,184,.18);box-shadow:0 14px 34px rgba(15,23,42,.06);transition:transform .18s ease,box-shadow .18s ease}.feature-card:hover{transform:translateY(-3px);box-shadow:0 20px 42px rgba(15,23,42,.10)}
|
||||
.feature-card:nth-child(1){background:linear-gradient(135deg,#eef7ff,#dceeff)}.feature-card:nth-child(2){background:linear-gradient(135deg,#effdf5,#d9f8e7)}.feature-card:nth-child(3){background:linear-gradient(135deg,#fff9ed,#ffedc9)}.feature-card:nth-child(4){background:linear-gradient(135deg,#f8f1ff,#eadcff)}.feature-card:nth-child(5){background:linear-gradient(135deg,#fff1f4,#ffdce4)}.feature-card:nth-child(6){background:linear-gradient(135deg,#eafffb,#ccf7ef)}
|
||||
.feature-card:nth-child(1) .feature-icon{background:#d7ebff;color:#1474e5}.feature-card:nth-child(2) .feature-icon{background:#c9f3dc;color:#079455}.feature-card:nth-child(3) .feature-icon{background:#ffe2aa;color:#e87500}.feature-card:nth-child(4) .feature-icon{background:#dfc9ff;color:#7c3aed}.feature-card:nth-child(5) .feature-icon{background:#ffc8d4;color:#e11d48}.feature-card:nth-child(6) .feature-icon{background:#bff2e9;color:#0891b2}
|
||||
input,select,textarea{width:100%;padding:11px 12px;border:1px solid #cbd5e1;border-radius:9px;margin:6px 0 14px;background:#fff;color:var(--text);font:inherit}textarea{resize:vertical;min-height:150px}select{appearance:auto}.field-error{color:var(--red);font-size:13px;margin:-8px 0 12px}.form-help{font-size:12px;color:var(--muted);margin:-8px 0 14px}.honeypot{position:absolute!important;left:-10000px!important;width:1px!important;height:1px!important;overflow:hidden!important}
|
||||
.support-hero{padding:8px 0 28px}.support-hero h1{font-size:44px;line-height:1.05;letter-spacing:-.04em;margin:8px 0 12px}.support-layout{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(280px,.7fr);gap:22px;align-items:start}.support-form-card{background:#fff;border:1px solid var(--line);border-radius:20px;padding:28px;box-shadow:var(--shadow-md)}.panel-heading-simple{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;padding-bottom:20px;border-bottom:1px solid var(--line);margin-bottom:22px}.panel-heading-simple h2{margin:0 0 4px;font-size:23px}.panel-heading-simple p{margin:0;color:var(--muted)}.response-badge{background:var(--blue-soft);color:var(--blue-dark);padding:6px 10px;border-radius:999px;font-size:12px;font-weight:800}.form-grid.two{display:grid;grid-template-columns:1fr 1fr;gap:16px}.consent-check{display:flex;align-items:flex-start;gap:10px;font-weight:500;color:#475467;margin:2px 0 20px}.consent-check input{width:18px;height:18px;margin:2px 0 0;flex:0 0 auto}.consent-check a{color:var(--blue);font-weight:700}.support-actions{display:flex;gap:12px;flex-wrap:wrap}.support-side{display:grid;gap:14px}.support-info-card{padding:22px;border-radius:18px;border:1px solid rgba(15,23,42,.08);box-shadow:var(--shadow-sm)}.support-info-card.blue{background:linear-gradient(135deg,#eff6ff,#dbeafe)}.support-info-card.green{background:linear-gradient(135deg,#ecfdf3,#d1fadf)}.support-info-card.violet{background:linear-gradient(135deg,#f5f3ff,#ede9fe)}.support-info-card h3{margin:12px 0 6px}.support-info-card p{margin:0;color:#475467}.support-info-icon{width:40px;height:40px;border-radius:11px;background:rgba(255,255,255,.75);display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:900;color:var(--blue)}
|
||||
.legal-page{max-width:840px;margin:0 auto;background:#fff;border:1px solid var(--line);border-radius:20px;padding:42px 46px;box-shadow:var(--shadow-sm)}.legal-head{padding-bottom:24px;border-bottom:1px solid var(--line);margin-bottom:26px}.legal-head h1{font-size:42px;line-height:1.05;letter-spacing:-.04em;margin:8px 0}.legal-head p{margin:0;color:var(--muted)}.legal-intro{font-size:18px;line-height:1.65;color:#344054;background:linear-gradient(135deg,#eff6ff,#f8fafc);border:1px solid #dbeafe;border-radius:14px;padding:18px 20px;margin-bottom:30px}.legal-page section{padding:4px 0 16px}.legal-page h2{font-size:21px;margin:16px 0 8px}.legal-page p{color:#475467;line-height:1.72}.legal-page a,.link-button{color:var(--blue);font-weight:700}.link-button{padding:0;background:none;border:0;box-shadow:none;text-decoration:underline;display:inline}.link-button:hover{background:none;color:var(--blue-dark)}.legal-table,.cookie-table{display:grid;gap:10px}.legal-table>div,.cookie-table>div{display:grid;grid-template-columns:150px 1fr;gap:18px;padding:14px 0;border-bottom:1px solid var(--line)}.legal-table span,.cookie-table span{color:#475467}
|
||||
.cookie-banner{position:fixed;z-index:1000;left:20px;right:20px;bottom:20px;max-width:980px;margin:auto;background:rgba(12,20,36,.98);color:#fff;border:1px solid rgba(255,255,255,.14);border-radius:18px;padding:20px 22px;box-shadow:0 24px 80px rgba(0,0,0,.38);backdrop-filter:blur(18px);display:none}.cookie-banner.is-open{display:block}.cookie-banner-grid{display:grid;grid-template-columns:1fr auto;gap:24px;align-items:center}.cookie-banner h2{margin:0 0 5px;font-size:18px}.cookie-banner p{margin:0;color:#cbd5e1;font-size:13px;line-height:1.55}.cookie-banner a{color:#bfdbfe}.cookie-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.cookie-actions button{padding:9px 12px}.cookie-actions .btn-secondary{background:transparent;color:#fff;border-color:rgba(255,255,255,.25)}
|
||||
.cookie-modal-backdrop{position:fixed;z-index:1100;inset:0;background:rgba(2,6,23,.64);display:none;align-items:center;justify-content:center;padding:20px}.cookie-modal-backdrop.is-open{display:flex}.cookie-modal{width:min(620px,100%);max-height:90vh;overflow:auto;background:#fff;border-radius:20px;box-shadow:0 34px 90px rgba(0,0,0,.35)}.cookie-modal-head{padding:24px 26px 18px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:18px}.cookie-modal-head h2{margin:0;font-size:24px}.cookie-close{background:#f2f4f7;color:#344054;width:36px;height:36px;padding:0;border-radius:50%}.cookie-close:hover{background:#e4e7ec}.cookie-modal-body{padding:8px 26px 4px}.cookie-option{display:grid;grid-template-columns:1fr auto;gap:18px;padding:17px 0;border-bottom:1px solid var(--line)}.cookie-option h3{margin:0 0 4px;font-size:16px}.cookie-option p{margin:0;color:var(--muted);font-size:13px}.cookie-switch{width:46px!important;height:24px!important;margin:2px 0!important;accent-color:var(--blue)}.cookie-modal-actions{padding:20px 26px 24px;display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}.cookie-required{font-size:12px;font-weight:800;color:var(--green);padding-top:4px}
|
||||
.admin-ticket-list{display:grid;gap:10px}.admin-ticket-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:18px;align-items:center;padding:16px 18px;background:#fff;border:1px solid var(--line);border-radius:14px;text-decoration:none;box-shadow:var(--shadow-sm)}.admin-ticket-row:hover{border-color:#b8cdf6}.admin-ticket-row>div:first-child{display:flex;flex-direction:column;min-width:0}.admin-ticket-row small{color:var(--muted)}.ticket-ref{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--blue);font-weight:850}.ticket-category,.ticket-status{padding:5px 8px;border-radius:999px;font-size:12px;font-weight:800;background:#f2f4f7;color:#475467}.status-new{background:#eff6ff;color:#1d4ed8}.status-open{background:#ecfdf3;color:#067647}.status-waiting{background:#fff7ed;color:#9a3412}.status-resolved,.status-closed{background:#f2f4f7;color:#475467}.admin-ticket-row time{color:var(--muted);font-size:12px}.filter-bar{display:flex;align-items:center;gap:10px;margin-bottom:18px}.filter-bar select{width:auto;margin:0}.ticket-detail-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:22px}.ticket-detail-head h1{font-size:34px;line-height:1.12;letter-spacing:-.03em;margin:6px 0}.ticket-detail-head p{margin:0;color:var(--muted)}.ticket-message .ticket-meta{display:flex;gap:12px;color:var(--muted);font-size:13px;margin:8px 0 20px}.message-body{white-space:normal;background:#f8fafc;border:1px solid var(--line);border-radius:12px;padding:18px;line-height:1.7;margin-bottom:18px}
|
||||
@media(max-width:800px){.support-layout{grid-template-columns:1fr}.form-grid.two{grid-template-columns:1fr}.legal-page{padding:28px 22px}.legal-table>div,.cookie-table>div{grid-template-columns:1fr;gap:4px}.cookie-banner-grid{grid-template-columns:1fr}.cookie-actions{justify-content:flex-start}.admin-ticket-row{grid-template-columns:1fr auto}.admin-ticket-row .ticket-category,.admin-ticket-row time{display:none}.ticket-detail-head{flex-direction:column}}
|
||||
|
||||
</style>
|
||||
@stack('head')
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="nav-shell" aria-label="Primary navigation">
|
||||
<a class="brand" href="{{ route('home') }}">
|
||||
<img src="{{ asset('icons/rsync-studio-icon-64.png') }}" alt="" width="34" height="34">
|
||||
<span>Rsync Studio</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="{{ route('home') }}">Product</a>
|
||||
<a href="{{ route('buy') }}">Pricing</a>
|
||||
<a href="{{ route('support') }}">Support</a>
|
||||
<a href="{{ route('privacy') }}">Privacy</a>
|
||||
@auth
|
||||
@if (auth()->user()->hasVerifiedEmail())
|
||||
<a href="{{ route('portal.index') }}">My licences</a>
|
||||
@endif
|
||||
|
||||
@if (auth()->user()->is_admin)
|
||||
@php
|
||||
$supportUnread = auth()->user()
|
||||
->unreadNotifications()
|
||||
->where('type', \App\Notifications\NewSupportTicket::class)
|
||||
->count();
|
||||
@endphp
|
||||
|
||||
<a href="{{ route('admin.index') }}">Admin</a>
|
||||
<a href="{{ route('admin.support.index') }}">
|
||||
Support
|
||||
@if ($supportUnread > 0)
|
||||
<span class="nav-alert-count">{{ $supportUnread }}</span>
|
||||
@endif
|
||||
</a>
|
||||
@endif
|
||||
@endauth
|
||||
</div>
|
||||
<div class="nav-account">
|
||||
@auth
|
||||
<span class="nav-email">{{ auth()->user()->email }}</span>
|
||||
<form method="post" action="{{ route('logout') }}">@csrf<button type="submit" class="nav-signout">Sign out</button></form>
|
||||
@else
|
||||
<a class="nav-auth-link" href="{{ route('login') }}">Sign in</a>
|
||||
<a class="nav-cta" href="{{ route('buy') }}">Start free trial</a>
|
||||
@endauth
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@hasSection('fullbleed')
|
||||
@yield('fullbleed')
|
||||
@else
|
||||
<main class="wrap">
|
||||
@if(session('status'))<div class="alert">{{ session('status') }}</div>@endif
|
||||
@if($errors->any())<div class="error">{{ $errors->first() }}</div>@endif
|
||||
@yield('content')
|
||||
</main>
|
||||
@endif
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-shell">
|
||||
<div>
|
||||
<div class="footer-brand"><img src="{{ asset('icons/rsync-studio-icon-64.png') }}" alt="" width="28" height="28"><span>Rsync Studio</span></div>
|
||||
<div class="footer-copy">Native rsync job management for macOS.</div>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="{{ route('home') }}">Product</a>
|
||||
<a href="{{ route('buy') }}">Pricing</a>
|
||||
<a href="{{ route('support') }}">Support</a>
|
||||
<a href="{{ route('privacy') }}">Privacy</a>
|
||||
<a href="{{ route('terms') }}">Terms</a>
|
||||
<a href="{{ route('cookies') }}">Cookies</a>
|
||||
<button type="button" class="footer-cookie-link" data-cookie-settings>Cookie settings</button>
|
||||
@auth @if(auth()->user()->hasVerifiedEmail())<a href="{{ route('portal.index') }}">Licence portal</a>@endif @else<a href="{{ route('login') }}">Sign in</a>@endauth
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<div class="cookie-banner" id="cookieBanner" role="region" aria-label="Cookie consent">
|
||||
<div class="cookie-banner-grid">
|
||||
<div>
|
||||
<h2>Your privacy choices</h2>
|
||||
<p>We use necessary cookies for security, sign-in and licence services. Optional cookies stay off unless you choose them. Read our <a href="{{ route('cookies') }}">Cookie Policy</a>.</p>
|
||||
</div>
|
||||
<div class="cookie-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cookieReject">Reject non-essential</button>
|
||||
<button type="button" class="btn btn-secondary" data-cookie-settings>Cookie settings</button>
|
||||
<button type="button" class="btn btn-primary" id="cookieAccept">Accept all</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cookie-modal-backdrop" id="cookieModal" role="presentation">
|
||||
<section class="cookie-modal" role="dialog" aria-modal="true" aria-labelledby="cookieModalTitle">
|
||||
<div class="cookie-modal-head"><div><span class="eyebrow">Privacy controls</span><h2 id="cookieModalTitle">Cookie settings</h2></div><button type="button" class="cookie-close" id="cookieClose" aria-label="Close cookie settings">×</button></div>
|
||||
<div class="cookie-modal-body">
|
||||
<div class="cookie-option"><div><h3>Necessary</h3><p>Required for security, authentication, sessions and remembering this choice.</p></div><span class="cookie-required">Always on</span></div>
|
||||
<label class="cookie-option"><div><h3>Preferences</h3><p>Allows non-essential interface preferences to be remembered.</p></div><input class="cookie-switch" type="checkbox" id="cookiePreferences"></label>
|
||||
<label class="cookie-option"><div><h3>Analytics</h3><p>Helps understand site usage. No analytics is loaded unless this is enabled.</p></div><input class="cookie-switch" type="checkbox" id="cookieAnalytics"></label>
|
||||
<label class="cookie-option"><div><h3>Marketing</h3><p>Allows optional marketing technologies. None is loaded unless this is enabled.</p></div><input class="cookie-switch" type="checkbox" id="cookieMarketing"></label>
|
||||
</div>
|
||||
<div class="cookie-modal-actions"><button type="button" class="btn btn-secondary" id="cookieRejectModal">Reject non-essential</button><button type="button" class="btn btn-primary" id="cookieSave">Save choices</button></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const COOKIE_NAME = 'rsync_cookie_consent_v{{ config('rsync-studio.privacy.cookie_version', '1') }}';
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const modal = document.getElementById('cookieModal');
|
||||
const prefs = document.getElementById('cookiePreferences');
|
||||
const analytics = document.getElementById('cookieAnalytics');
|
||||
const marketing = document.getElementById('cookieMarketing');
|
||||
|
||||
const read = () => {
|
||||
const match = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||||
if (!match) return null;
|
||||
try { return JSON.parse(decodeURIComponent(match.substring(COOKIE_NAME.length + 1))); } catch (_) { return null; }
|
||||
};
|
||||
const write = (value) => {
|
||||
const secure = location.protocol === 'https:' ? '; Secure' : '';
|
||||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(value)) + '; Max-Age=31536000; Path=/; SameSite=Lax' + secure;
|
||||
window.rsyncCookieConsent = value;
|
||||
window.dispatchEvent(new CustomEvent('rsync:cookie-consent', {detail:value}));
|
||||
banner.classList.remove('is-open'); modal.classList.remove('is-open'); document.body.style.overflow = '';
|
||||
};
|
||||
const reject = () => write({necessary:true,preferences:false,analytics:false,marketing:false,updatedAt:new Date().toISOString()});
|
||||
const open = () => { const current = read() || {}; prefs.checked=!!current.preferences; analytics.checked=!!current.analytics; marketing.checked=!!current.marketing; modal.classList.add('is-open'); document.body.style.overflow='hidden'; };
|
||||
const close = () => { modal.classList.remove('is-open'); document.body.style.overflow=''; };
|
||||
|
||||
document.querySelectorAll('[data-cookie-settings]').forEach(el => el.addEventListener('click', open));
|
||||
document.getElementById('cookieAccept').addEventListener('click', () => write({necessary:true,preferences:true,analytics:true,marketing:true,updatedAt:new Date().toISOString()}));
|
||||
document.getElementById('cookieReject').addEventListener('click', reject);
|
||||
document.getElementById('cookieRejectModal').addEventListener('click', reject);
|
||||
document.getElementById('cookieSave').addEventListener('click', () => write({necessary:true,preferences:prefs.checked,analytics:analytics.checked,marketing:marketing.checked,updatedAt:new Date().toISOString()}));
|
||||
document.getElementById('cookieClose').addEventListener('click', close);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) close(); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.classList.contains('is-open')) close(); });
|
||||
|
||||
const current = read();
|
||||
window.rsyncCookieConsent = current || {necessary:true,preferences:false,analytics:false,marketing:false};
|
||||
if (!current) banner.classList.add('is-open');
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@extends('layouts.app', ['title' => 'Cookie Policy — Rsync Studio'])
|
||||
|
||||
@section('content')
|
||||
<article class="legal-page">
|
||||
<header class="legal-head"><span class="eyebrow">Privacy controls</span><h1>Cookie Policy</h1><p>Last updated 9 September 2026</p></header>
|
||||
<div class="legal-intro">Rsync Studio uses a small number of cookies or equivalent browser storage items. Optional categories are off by default until you consent.</div>
|
||||
|
||||
<section><h2>Necessary</h2><p>These are required for security and core site features and cannot be disabled through Cookie Settings.</p><div class="cookie-table"><div><strong>Session cookie</strong><span>Keeps you signed in and maintains the secure server session. Session or configured server lifetime.</span></div><div><strong>XSRF / CSRF protection</strong><span>Helps protect forms and authenticated actions against cross-site request forgery. Session or configured framework lifetime.</span></div><div><strong>Cookie consent preference</strong><span>Stores the categories you selected so we do not ask on every page. Up to 12 months.</span></div></div></section>
|
||||
|
||||
<section><h2>Preferences</h2><p>Preference storage is optional and disabled unless you enable it. It may be used for non-essential interface choices in future versions of the site.</p></section>
|
||||
<section><h2>Analytics</h2><p>Analytics is currently disabled by default. If analytics tooling is added, it must only load after you enable Analytics in Cookie Settings and this policy must be updated to identify the provider and retention period.</p></section>
|
||||
<section><h2>Marketing</h2><p>Marketing storage is currently disabled by default. Marketing or advertising technology must not load before you expressly enable this category.</p></section>
|
||||
<section><h2>Paddle checkout</h2><p>When you choose to start checkout, Paddle's checkout is loaded to provide payment and subscription functionality. Paddle may use storage that is necessary to provide checkout, prevent fraud and comply with payment obligations. Paddle provides its own privacy and cookie information for its service.</p></section>
|
||||
<section><h2>Change your choice</h2><p>You can revisit your choice at any time.</p><button type="button" class="btn btn-secondary" data-cookie-settings>Open Cookie Settings</button></section>
|
||||
</article>
|
||||
@endsection
|
||||
@@ -0,0 +1,39 @@
|
||||
@extends('layouts.app', ['title' => 'Privacy Policy — Rsync Studio'])
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$legal = config('rsync-studio.legal');
|
||||
$retention = config('rsync-studio.privacy.support_retention_days', 730);
|
||||
@endphp
|
||||
<article class="legal-page">
|
||||
<header class="legal-head">
|
||||
<span class="eyebrow">Privacy</span>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Last updated 9 September 2026 · Policy version {{ config('rsync-studio.privacy.policy_version') }}</p>
|
||||
</header>
|
||||
|
||||
<div class="legal-intro">{{ $legal['controller_name'] }} is the data controller for personal data processed through the Rsync Studio website, licence portal and support service. We process only the information needed to sell, license, secure and support Rsync Studio.</div>
|
||||
|
||||
<section><h2>Information we process</h2><p>We may process your name, email address, account credentials in hashed form, licence and activation identifiers, subscription and billing status, support requests, and technical server logs required to operate and secure the service. Rsync Studio does not send the files you sync, SSH passwords or private SSH keys to the licensing service.</p></section>
|
||||
|
||||
<section><h2>Payments</h2><p>Payments and subscription checkout are handled by Paddle. Payment-card details are entered into Paddle's checkout and are not stored by Rsync Studio. We receive identifiers and subscription information needed to provision and manage your licence. Paddle acts independently for parts of its payment and tax processing and provides its own privacy information.</p></section>
|
||||
|
||||
<section><h2>Why we use your information</h2><div class="legal-table"><div><strong>Contract</strong><span>Creating your account, providing licences, validating activations, managing subscriptions and delivering support you request.</span></div><div><strong>Legitimate interests</strong><span>Securing the service, preventing abuse, troubleshooting faults and maintaining reliable operational records where those interests do not override your rights.</span></div><div><strong>Legal obligations</strong><span>Records we must keep for accounting, tax, fraud prevention or other applicable legal requirements.</span></div><div><strong>Consent</strong><span>Any optional analytics or marketing storage, if introduced, is disabled until you choose to enable it through Cookie Settings.</span></div></div></section>
|
||||
|
||||
<section><h2>Support requests</h2><p>Support requests are stored in our database and emailed to the configured support team. We retain resolved support tickets for up to {{ $retention }} days, unless a longer period is necessary for an unresolved dispute, security matter or legal obligation. A scheduled retention process removes eligible resolved tickets.</p></section>
|
||||
|
||||
<section><h2>Licence and account records</h2><p>Licence, activation and subscription records are kept while your subscription or licence relationship is active and for a reasonable period afterwards where required for licence recovery, fraud prevention, accounting, dispute handling and legal obligations. We minimise retained data and do not use licence validation to inspect your synced files.</p></section>
|
||||
|
||||
<section><h2>Who receives data</h2><p>Information may be processed by service providers that are necessary to run Rsync Studio, such as Paddle for checkout and subscription processing, our hosting/infrastructure providers, and email delivery infrastructure. We limit disclosure to what is necessary for the relevant service and require appropriate safeguards where applicable.</p></section>
|
||||
|
||||
<section><h2>International transfers</h2><p>Some service providers may process information outside the UK or EEA. Where data-protection law requires it, appropriate transfer mechanisms and safeguards must be used by the relevant controller or processor.</p></section>
|
||||
|
||||
<section><h2>Cookies and local storage</h2><p>Necessary cookies are used for security, authentication, sessions and your privacy preferences. Optional analytics and marketing categories default to off and must not be loaded before consent. See our <a href="{{ route('cookies') }}">Cookie Policy</a> or use <button type="button" class="link-button" data-cookie-settings>Cookie Settings</button>.</p></section>
|
||||
|
||||
<section><h2>Your rights</h2><p>Depending on the circumstances, you may have rights to access, correct, erase, restrict or object to processing of your personal data, request portability, and withdraw consent at any time where processing relies on consent. You also have the right to complain to the UK Information Commissioner's Office or your applicable supervisory authority.</p><p>Use the <a href="{{ route('support') }}">support form</a> and choose “Privacy / data request”. We may need to verify your identity before acting on a request.</p></section>
|
||||
|
||||
<section><h2>Security</h2><p>We use access controls, TLS, password hashing, CSRF protection, rate limiting and restricted administrative access. No internet service can guarantee absolute security, so we also minimise the personal data we collect.</p></section>
|
||||
|
||||
<section><h2>Contact</h2><p>Controller: <strong>{{ $legal['controller_name'] }}</strong><br>Country: {{ $legal['country'] }}<br>Privacy contact: <a href="mailto:{{ $legal['privacy_email'] }}">{{ $legal['privacy_email'] }}</a></p></section>
|
||||
</article>
|
||||
@endsection
|
||||
@@ -0,0 +1,19 @@
|
||||
@extends('layouts.app', ['title' => 'Terms of Service — Rsync Studio'])
|
||||
|
||||
@section('content')
|
||||
@php($legal = config('rsync-studio.legal'))
|
||||
<article class="legal-page">
|
||||
<header class="legal-head"><span class="eyebrow">Legal</span><h1>Terms of Service</h1><p>Last updated 9 September 2026</p></header>
|
||||
<div class="legal-intro">These terms govern your use of the Rsync Studio website, licence portal, subscription service and macOS application.</div>
|
||||
<section><h2>1. Licence plans</h2><p>Personal permits the number of active Mac installations shown for that plan at purchase. Professional permits the number shown for Professional. A licence is for your use and may not be shared, resold or used to bypass activation limits.</p></section>
|
||||
<section><h2>2. Trials and subscriptions</h2><p>Where offered, the trial length and renewal terms are shown at checkout. Subscription billing, cancellation, tax and payment processing are handled through Paddle. Your subscription continues until cancelled in accordance with the checkout terms. Changes between Personal and Professional may be prorated by Paddle.</p></section>
|
||||
<section><h2>3. Activation</h2><p>Rsync Studio uses a licence service to activate permitted Macs and periodically confirm that an activation remains valid. A reasonable offline grace period may be provided, but continued use after a subscription or activation is revoked is not authorised.</p></section>
|
||||
<section><h2>4. Your rsync jobs</h2><p>You are responsible for choosing correct source and destination paths, permissions, credentials, rsync options and deletion behaviour. Dry Run is provided to help review operations before execution, but you remain responsible for backups and for verifying important transfers.</p></section>
|
||||
<section><h2>5. Acceptable use</h2><p>You must not attempt to defeat licensing controls, interfere with the service, probe accounts you do not own, distribute malware, or use Rsync Studio in violation of applicable law or third-party rights.</p></section>
|
||||
<section><h2>6. Updates and availability</h2><p>We may update the application, website and licensing service to improve security, compatibility or functionality. We aim to provide a reliable service but do not guarantee uninterrupted availability.</p></section>
|
||||
<section><h2>7. Consumer rights</h2><p>Nothing in these terms excludes rights that cannot lawfully be excluded, including applicable UK consumer rights. Where mandatory law gives you stronger protections, that law prevails.</p></section>
|
||||
<section><h2>8. Liability</h2><p>To the extent permitted by law, we are not responsible for indirect or consequential loss arising from incorrect sync configuration, deleted destination data, unavailable third-party systems or failures outside our reasonable control. This does not limit liability that cannot lawfully be limited.</p></section>
|
||||
<section><h2>9. Privacy</h2><p>Our processing of personal data is described in the <a href="{{ route('privacy') }}">Privacy Policy</a> and <a href="{{ route('cookies') }}">Cookie Policy</a>.</p></section>
|
||||
<section><h2>10. Contact</h2><p>These terms are provided by {{ $legal['controller_name'] }} in {{ $legal['country'] }}. Questions can be submitted through <a href="{{ route('support') }}">Rsync Studio Support</a>.</p></section>
|
||||
</article>
|
||||
@endsection
|
||||
Regular → Executable
+113
-1
@@ -1 +1,113 @@
|
||||
@extends('layouts.app') @section('content')<h1>My licences</h1>@forelse($licenses as $license)<div class="card"><h2>{{ $license->product->name }} · {{ $license->edition }}</h2><p>Licence ending <strong>{{ $license->license_key_last4 }}</strong> · {{ $license->activations->whereNull('deactivated_at')->count() }}/{{ $license->activation_limit }} activations</p><p class="{{ $license->is_usable ? 'ok':'bad' }}">{{ $license->is_usable ? 'Active':'Unavailable' }}</p><a class="btn" href="{{ route('portal.show',$license) }}">Manage</a></div>@empty<div class="card">No licences are associated with {{ auth()->user()->email }}.</div>@endforelse@endsection
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<section class="portal-header">
|
||||
<div>
|
||||
<span class="eyebrow">Account</span>
|
||||
<h1>My licences</h1>
|
||||
<p class="lead compact">Manage subscriptions, licence keys and activated Macs.</p>
|
||||
</div>
|
||||
<a class="btn btn-secondary" href="{{ route('buy') }}">Buy another licence</a>
|
||||
</section>
|
||||
|
||||
@php
|
||||
$activeLicences = $licenses->filter(fn ($license) => $license->is_usable);
|
||||
$historyLicences = $licenses->reject(fn ($license) => $license->is_usable);
|
||||
@endphp
|
||||
|
||||
@if($activeLicences->isEmpty())
|
||||
<div class="empty-state">
|
||||
<h2>No active licences</h2>
|
||||
<p>No active Rsync Studio licence is associated with {{ auth()->user()->email }}.</p>
|
||||
<a class="btn" href="{{ route('buy') }}">Choose a plan</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="licence-list">
|
||||
@foreach($activeLicences as $license)
|
||||
@php
|
||||
$activeCount = $license->activations->whereNull('deactivated_at')->count();
|
||||
$isDuplicatePersonal = $hasProfessional && $license->edition === 'Personal';
|
||||
$billing = str_contains(strtolower((string) optional($license->product)->code), 'annual') ? 'Annual' : 'Monthly';
|
||||
@endphp
|
||||
<article class="licence-card {{ $license->edition === 'Professional' ? 'licence-card-featured' : '' }}">
|
||||
<div class="licence-main">
|
||||
<div class="licence-title-row">
|
||||
<div>
|
||||
<div class="badge-row">
|
||||
<span class="status-badge status-active">Active</span>
|
||||
<span class="plan-badge">{{ $license->edition }}</span>
|
||||
<span class="billing-badge">{{ $billing }}</span>
|
||||
</div>
|
||||
<h2>Rsync Studio {{ $license->edition }}</h2>
|
||||
<p class="muted licence-key">Licence ending <strong>{{ $license->license_key_last4 }}</strong></p>
|
||||
</div>
|
||||
<a class="btn btn-secondary" href="{{ route('portal.show', $license) }}">Manage licence</a>
|
||||
</div>
|
||||
|
||||
<div class="licence-stats">
|
||||
<div>
|
||||
<span class="stat-label">Macs activated</span>
|
||||
<strong>{{ $activeCount }} of {{ $license->activation_limit }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="stat-label">Subscription</span>
|
||||
<strong>{{ ucfirst($license->subscription_status ?: 'active') }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="stat-label">{{ $license->subscription_status === 'trialing' ? 'Trial ends' : 'Renews' }}</span>
|
||||
<strong>{{ optional($license->current_period_ends_at)->format('j M Y') ?: '—' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activation-meter" aria-label="{{ $activeCount }} of {{ $license->activation_limit }} Macs activated">
|
||||
<span style="width: {{ min(100, ($activeCount / max(1, $license->activation_limit)) * 100) }}%"></span>
|
||||
</div>
|
||||
|
||||
@if($isDuplicatePersonal)
|
||||
<div class="subscription-warning">
|
||||
<div>
|
||||
<strong>You already have an active Professional subscription.</strong>
|
||||
<p>This Personal plan is a separate Paddle subscription and could be billed separately. Cancel it if it was only created while upgrading.</p>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.cancel', $license) }}" onsubmit="return confirm('Cancel this Personal subscription immediately in Paddle? This cannot be undone.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="danger">Cancel Personal</button>
|
||||
</form>
|
||||
</div>
|
||||
@elseif($license->edition === 'Personal')
|
||||
<div class="upgrade-strip">
|
||||
<div>
|
||||
<strong>Need more Macs?</strong>
|
||||
<span>Upgrade this subscription to Professional for up to 3 Macs.</span>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.upgrade', $license) }}" onsubmit="return confirm('Upgrade this existing Paddle subscription to Professional now? Paddle will prorate the change.');">
|
||||
@csrf
|
||||
<button>Upgrade to Professional</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($historyLicences->isNotEmpty())
|
||||
<details class="history-panel">
|
||||
<summary>Licence history <span>{{ $historyLicences->count() }}</span></summary>
|
||||
<div class="history-list">
|
||||
@foreach($historyLicences as $license)
|
||||
<div class="history-row">
|
||||
<div>
|
||||
<strong>Rsync Studio {{ $license->edition }}</strong>
|
||||
<span>Licence ending {{ $license->license_key_last4 }}</span>
|
||||
</div>
|
||||
<div class="history-status">{{ ucfirst($license->subscription_status ?: ($license->revoked_at ? 'revoked' : 'unavailable')) }}</div>
|
||||
<a href="{{ route('portal.show', $license) }}">Details</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
Regular → Executable
+80
-1
@@ -1 +1,80 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>{{ $license->product->name }}</h1><p>{{ $license->edition }} · key ending {{ $license->license_key_last4 }}</p><form method="post" action="{{ route('portal.resend',$license) }}">@csrf<button>Email my licence key</button></form></div><div class="card"><h2>Installations</h2><table><tr><th>Installation</th><th>Platform</th><th>Version</th><th>Last seen</th><th></th></tr>@foreach($license->activations->whereNull('deactivated_at') as $a)<tr><td>{{ $a->installation_name ?: $a->installation_id }}</td><td>{{ $a->platform }}</td><td>{{ $a->app_version }}</td><td>{{ optional($a->last_seen_at)->toDayDateTimeString() }}</td><td><form method="post" action="{{ route('portal.deactivate',[$license,$a->id]) }}">@csrf @method('DELETE')<button class="danger">Deactivate</button></form></td></tr>@endforeach</table></div>@endsection
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$activeActivations = $license->activations->whereNull('deactivated_at');
|
||||
$billing = str_contains(strtolower((string) optional($license->product)->code), 'annual') ? 'Annual' : 'Monthly';
|
||||
@endphp
|
||||
|
||||
<a class="back-link" href="{{ route('portal.index') }}">← My licences</a>
|
||||
|
||||
<section class="licence-detail-head">
|
||||
<div>
|
||||
<div class="badge-row">
|
||||
<span class="status-badge {{ $license->is_usable ? 'status-active' : 'status-inactive' }}">{{ $license->is_usable ? 'Active' : 'Unavailable' }}</span>
|
||||
<span class="plan-badge">{{ $license->edition }}</span>
|
||||
<span class="billing-badge">{{ $billing }}</span>
|
||||
</div>
|
||||
<h1>Rsync Studio {{ $license->edition }}</h1>
|
||||
<p class="muted">Licence ending <strong>{{ $license->license_key_last4 }}</strong></p>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.resend', $license) }}">
|
||||
@csrf
|
||||
<button class="btn-secondary">Email licence key</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="detail-grid">
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>Activated Macs</h2>
|
||||
<p>{{ $activeActivations->count() }} of {{ $license->activation_limit }} activation slots in use.</p>
|
||||
</div>
|
||||
<span class="activation-count">{{ $activeActivations->count() }}/{{ $license->activation_limit }}</span>
|
||||
</div>
|
||||
|
||||
@forelse($activeActivations as $activation)
|
||||
<div class="device-row">
|
||||
<div class="device-icon">⌘</div>
|
||||
<div class="device-copy">
|
||||
<strong>{{ $activation->installation_name ?: 'Mac' }}</strong>
|
||||
<span>{{ $activation->platform ?: 'macOS' }}@if($activation->app_version) · Rsync Studio {{ $activation->app_version }}@endif</span>
|
||||
<small>Last seen {{ optional($activation->last_seen_at)->diffForHumans() ?: 'not yet recorded' }}</small>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.deactivate', [$license, $activation->id]) }}" onsubmit="return confirm('Deactivate this Mac? Rsync Studio on that Mac will lose access when it next validates.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="danger-outline">Deactivate</button>
|
||||
</form>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-inline">No Macs are currently activated with this licence.</div>
|
||||
@endforelse
|
||||
</section>
|
||||
|
||||
<aside class="panel subscription-panel">
|
||||
<h2>Subscription</h2>
|
||||
<dl class="subscription-facts">
|
||||
<div><dt>Plan</dt><dd>{{ $license->edition }} {{ $billing }}</dd></div>
|
||||
<div><dt>Status</dt><dd>{{ ucfirst($license->subscription_status ?: 'active') }}</dd></div>
|
||||
<div><dt>{{ $license->subscription_status === 'trialing' ? 'Trial ends' : 'Current period ends' }}</dt><dd>{{ optional($license->current_period_ends_at)->format('j M Y') ?: '—' }}</dd></div>
|
||||
</dl>
|
||||
|
||||
@if($license->edition === 'Personal' && $license->is_usable)
|
||||
<form method="post" action="{{ route('portal.upgrade', $license) }}" onsubmit="return confirm('Upgrade this existing Paddle subscription to Professional now? Paddle will prorate the change.');">
|
||||
@csrf
|
||||
<button class="full-width">Upgrade to Professional</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if($license->payment_provider === 'paddle' && $license->external_subscription_id && $license->subscription_status !== 'canceled')
|
||||
<form method="post" action="{{ route('portal.cancel', $license) }}" onsubmit="return confirm('Cancel this Paddle subscription immediately? This cannot be undone.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="danger-outline full-width">Cancel subscription</button>
|
||||
</form>
|
||||
@endif
|
||||
</aside>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
Regular → Executable
+147
-1
@@ -1 +1,147 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Rsync Studio licensing</h1><p>Manage your Rsync Studio licence, installations and recovery options securely.</p>@guest<a class="btn" href="{{ route('login') }}">Sign in</a>@endguest</div>@endsection
|
||||
@extends('layouts.app', [
|
||||
'title' => 'Rsync Studio — Native rsync for macOS',
|
||||
'description' => 'Create, schedule, watch and review local and SSH rsync jobs with a native macOS interface. Start a 7-day free trial.'
|
||||
])
|
||||
|
||||
@section('fullbleed')
|
||||
<main class="launch-main">
|
||||
<section class="launch-hero">
|
||||
<div class="launch-shell hero-grid">
|
||||
<div>
|
||||
<span class="launch-kicker">Native macOS rsync management</span>
|
||||
<h1>Rsync without the command-line friction.</h1>
|
||||
<p class="hero-copy">Build reliable local and SSH sync jobs, test them safely with Dry Run, schedule recurring transfers, watch folders for changes and keep a complete run history — all from one native Mac app.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-primary" href="{{ route('buy') }}">Start 7-day free trial</a>
|
||||
@guest<a class="btn btn-secondary" href="{{ route('login') }}">Open licence portal</a>@else<a class="btn btn-secondary" href="{{ route('portal.index') }}">Open my licences</a>@endguest
|
||||
</div>
|
||||
<p class="hero-note">Personal for 1 Mac · Professional for up to 3 Macs · Monthly or annual</p>
|
||||
<div class="trust-row" aria-label="Product highlights">
|
||||
<span class="trust-item"><span class="trust-check">✓</span> Native macOS app</span>
|
||||
<span class="trust-item"><span class="trust-check">✓</span> Local & SSH jobs</span>
|
||||
<span class="trust-item"><span class="trust-check">✓</span> Dry Run safeguards</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-window" aria-label="Rsync Studio application preview">
|
||||
<div class="window-bar">
|
||||
<span class="dot red"></span><span class="dot amber"></span><span class="dot green"></span>
|
||||
<span class="window-title">Rsync Studio — Jobs</span>
|
||||
</div>
|
||||
<div class="app-body">
|
||||
<aside class="app-sidebar">
|
||||
<div class="mini-brand"><img src="{{ asset('icons/rsync-studio-icon-64.png') }}" alt=""><span>Rsync Studio</span></div>
|
||||
<div class="side-item active">Jobs</div>
|
||||
<div class="side-item">Calendar</div>
|
||||
<div class="side-item">History</div>
|
||||
<div class="side-item">Settings</div>
|
||||
</aside>
|
||||
<div class="app-content">
|
||||
<div class="app-toolbar"><strong>Sync jobs</strong><span class="sync-all">Sync All</span></div>
|
||||
<div class="mock-job">
|
||||
<div class="mock-job-top"><strong>Developer</strong><span class="job-state state-ready">Ready</span></div>
|
||||
<p>Developer folder → remote server · SSH</p>
|
||||
<div class="mock-progress"><span style="width:100%"></span></div>
|
||||
</div>
|
||||
<div class="mock-job">
|
||||
<div class="mock-job-top"><strong>Web Sites</strong><span class="job-state state-scheduled">22:00</span></div>
|
||||
<p>Sites folder → remote server · Daily</p>
|
||||
<div class="mock-progress"><span style="width:72%"></span></div>
|
||||
</div>
|
||||
<div class="mock-job">
|
||||
<div class="mock-job-top"><strong>Project Watch</strong><span class="job-state state-watching">Watching</span></div>
|
||||
<p>Project folder · File watch</p>
|
||||
<div class="mock-progress"><span style="width:44%"></span></div>
|
||||
</div>
|
||||
<div class="app-statusbar"><span>Last run: Success</span><span>rsync 3.4.1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="launch-section">
|
||||
<div class="section-shell">
|
||||
<div class="section-head center">
|
||||
<span class="eyebrow">Everything in one place</span>
|
||||
<h2>Built for everyday rsync work.</h2>
|
||||
<p>Keep the power of rsync while replacing repetitive commands and scattered scripts with a clear, repeatable workflow.</p>
|
||||
</div>
|
||||
<div class="feature-grid">
|
||||
<article class="feature-card"><div class="feature-icon">↔</div><h3>Local & SSH sync</h3><p>Create local copies or securely sync to remote Macs and Linux servers over SSH.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">✓</div><h3>Dry Run first</h3><p>Preview changes before files are transferred or deleted, so risky jobs can be checked before they run.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">◷</div><h3>Flexible schedules</h3><p>Run jobs once, hourly, daily, weekly, monthly or on your own recurring interval.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">⌁</div><h3>Watch for changes</h3><p>Use macOS file events to trigger selected jobs when files or folders change.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">≡</div><h3>Complete run history</h3><p>Review commands, results, stdout, stderr and transfer statistics for previous runs.</p></article>
|
||||
<article class="feature-card"><div class="feature-icon">⌘</div><h3>Mac-first experience</h3><p>Manage jobs, defaults, background operation and connection settings from a native macOS interface.</p></article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="launch-section workflow-section">
|
||||
<div class="section-shell">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">A safer workflow</span>
|
||||
<h2>From setup to sync in four clear steps.</h2>
|
||||
</div>
|
||||
<div class="workflow-grid">
|
||||
<article class="workflow-step"><div class="step-number">1</div><h3>Create a job</h3><p>Choose your source, destination and rsync options once.</p></article>
|
||||
<article class="workflow-step"><div class="step-number">2</div><h3>Run a Dry Run</h3><p>Check the generated operation before changing any files.</p></article>
|
||||
<article class="workflow-step"><div class="step-number">3</div><h3>Run or automate</h3><p>Start it manually, schedule it or attach a file watcher.</p></article>
|
||||
<article class="workflow-step"><div class="step-number">4</div><h3>Review history</h3><p>See the result and retain the command and transfer output.</p></article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="launch-section">
|
||||
<div class="section-shell">
|
||||
<div class="security-panel">
|
||||
<div>
|
||||
<span class="eyebrow" style="color:#93c5fd">Designed around your Mac</span>
|
||||
<h2>Your sync workflow stays direct.</h2>
|
||||
<p>Rsync Studio runs rsync and SSH from your Mac. The licensing service is separate from your file-transfer path.</p>
|
||||
</div>
|
||||
<div class="security-list">
|
||||
<div class="security-item"><div class="security-icon">K</div><div><strong>Keychain-aware settings</strong><span>Stored passwords use macOS Keychain rather than application preferences.</span></div></div>
|
||||
<div class="security-item"><div class="security-icon">S</div><div><strong>SSH-native connections</strong><span>Use the standard macOS SSH client, keys and known-host verification for remote jobs.</span></div></div>
|
||||
<div class="security-item"><div class="security-icon">D</div><div><strong>Dry Run safeguards</strong><span>Test jobs before enabling destructive options such as destination deletion.</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="launch-section" id="pricing">
|
||||
<div class="section-shell">
|
||||
<div class="section-head center">
|
||||
<span class="eyebrow">Simple licensing</span>
|
||||
<h2>Choose the number of Macs you need.</h2>
|
||||
<p>Both plans include the full Rsync Studio workflow and a 7-day free trial. Checkout shows pricing in your supported local currency.</p>
|
||||
</div>
|
||||
<div class="plan-grid">
|
||||
<article class="launch-plan">
|
||||
<h3>Personal</h3>
|
||||
<p>For a single Mac and an individual sync workflow.</p>
|
||||
<div class="plan-macs">1 Mac</div><div class="plan-sub">Monthly or annual subscription</div>
|
||||
<ul class="plan-list"><li>Local and SSH jobs</li><li>Scheduling and file watch</li><li>Dry Run and history</li><li>7-day free trial</li></ul>
|
||||
<a class="btn btn-secondary" href="{{ route('buy') }}">Choose Personal</a>
|
||||
</article>
|
||||
<article class="launch-plan featured">
|
||||
<span class="best-badge">Most flexible</span>
|
||||
<h3>Professional</h3>
|
||||
<p>For multiple Macs or a broader development workflow.</p>
|
||||
<div class="plan-macs">Up to 3 Macs</div><div class="plan-sub">Monthly or annual subscription</div>
|
||||
<ul class="plan-list"><li>Everything in Personal</li><li>Three active Mac installations</li><li>Manage activations from the portal</li><li>7-day free trial</li></ul>
|
||||
<a class="btn btn-primary" href="{{ route('buy') }}">Choose Professional</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="launch-cta">
|
||||
<div class="cta-box">
|
||||
<div><h2>Make rsync easier to run every day.</h2><p>Start with a 7-day free trial and choose Personal or Professional at checkout.</p></div>
|
||||
<a class="btn" href="{{ route('buy') }}">Start free trial</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@endsection
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
@extends('layouts.app', [
|
||||
'title' => 'Support — Rsync Studio',
|
||||
'description' => 'Contact Rsync Studio support for technical, licence, billing and privacy enquiries.'
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<section class="support-hero">
|
||||
<span class="eyebrow">Support</span>
|
||||
<h1>How can we help?</h1>
|
||||
<p class="lead">Send us the details below. Your request is saved securely, assigned a reference and emailed to the support team.</p>
|
||||
</section>
|
||||
|
||||
<div class="support-layout">
|
||||
<section class="support-form-card">
|
||||
<div class="panel-heading-simple">
|
||||
<div>
|
||||
<h2>Contact support</h2>
|
||||
<p>We will reply to the email address you provide.</p>
|
||||
</div>
|
||||
<span class="response-badge">Support ticket</span>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('support.store') }}" class="support-form">
|
||||
@csrf
|
||||
<div class="honeypot" aria-hidden="true">
|
||||
<label for="company_website">Company website</label>
|
||||
<input id="company_website" name="company_website" type="text" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-grid two">
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" value="{{ old('name', auth()->user()?->name) }}" autocomplete="name" required maxlength="120">
|
||||
@error('name')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="email" name="email" value="{{ old('email', auth()->user()?->email) }}" autocomplete="email" required maxlength="254">
|
||||
@error('email')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid two">
|
||||
<div>
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category" required>
|
||||
@foreach($categories as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('category') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('category')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject">Subject</label>
|
||||
<input id="subject" name="subject" value="{{ old('subject') }}" required maxlength="180" placeholder="Briefly describe the issue">
|
||||
@error('subject')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message">Message</label>
|
||||
<textarea id="message" name="message" rows="8" required maxlength="10000" placeholder="Tell us what happened, what you expected, your macOS version and Rsync Studio version where relevant.">{{ old('message') }}</textarea>
|
||||
<div class="form-help">Do not include passwords, private SSH keys or your full licence token.</div>
|
||||
@error('message')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<label class="consent-check">
|
||||
<input type="checkbox" name="privacy_notice" value="1" @checked(old('privacy_notice')) required>
|
||||
<span>I have read the <a href="{{ route('privacy') }}" target="_blank" rel="noopener">Privacy Policy</a> and understand that my details will be used to respond to this request.</span>
|
||||
</label>
|
||||
@error('privacy_notice')<div class="field-error">{{ $message }}</div>@enderror
|
||||
|
||||
<div class="support-actions">
|
||||
<button type="submit" class="btn btn-primary">Send support request</button>
|
||||
@auth
|
||||
@if(auth()->user()->hasVerifiedEmail())<a class="btn btn-secondary" href="{{ route('portal.index') }}">Open licence portal</a>@endif
|
||||
@endauth
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="support-side">
|
||||
<article class="support-info-card blue">
|
||||
<div class="support-info-icon">◎</div>
|
||||
<h3>Licence & activation</h3>
|
||||
<p>Include the last four characters of your licence key and the exact activation message.</p>
|
||||
</article>
|
||||
<article class="support-info-card green">
|
||||
<div class="support-info-icon">✓</div>
|
||||
<h3>Useful diagnostics</h3>
|
||||
<p>Include the Rsync Studio version, macOS version and the relevant History error output.</p>
|
||||
</article>
|
||||
<article class="support-info-card violet">
|
||||
<div class="support-info-icon">⌁</div>
|
||||
<h3>Privacy requests</h3>
|
||||
<p>Choose “Privacy / data request” for access, correction, deletion, restriction or objection requests.</p>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
@endsection
|
||||
Regular → Executable
Reference in New Issue
Block a user