feat(developers): add developer program signup with SilverDESK integration

Add developer application page with form submission that creates tickets
in SilverDESK. Includes provisioning service scaffolding for Mattermost,
Mailcow, and Gitea account creation. Fixes API key header casing
(X-API-Key) and ticket payload to match SilverDESK's CreateTicketDto
contract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 20:51:36 +00:00
parent d2780e9295
commit 21c07adf54
11 changed files with 1056 additions and 3 deletions

View File

@@ -7,6 +7,7 @@
<base href="/" />
<link rel="stylesheet" href="@Assets["styles.css"]" />
<link rel="stylesheet" href="@Assets["sdk-styles.css"]" />
<link rel="stylesheet" href="@Assets["developers-styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />

View File

@@ -0,0 +1,244 @@
@page "/developers"
@using SilverLabs.Website.Models
@using SilverLabs.Website.Services
@inject DeveloperApplicationService ApplicationService
@rendermode InteractiveServer
<PageTitle>Join the Team - SilverLabs</PageTitle>
<div class="main-content visible">
<header class="header">
<img src="logo.png" alt="SilverLabs Logo" class="logo">
</header>
<div class="dev-container">
<div class="dev-header">
<h1>Join the SilverLabs Team</h1>
<p class="dev-subtitle">Help us build privacy-first infrastructure. Whether you test our products or write code, there's a place for you.</p>
</div>
@if (_submitted)
{
<div class="dev-success-panel">
<div class="success-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h2>Application Submitted</h2>
<p>@_resultMessage</p>
<a href="/" class="dev-btn dev-btn-secondary">Back to Home</a>
</div>
}
else
{
<EditForm Model="_application" OnValidSubmit="HandleSubmit" FormName="developer-application">
<DataAnnotationsValidator />
<!-- Role Selector -->
<div class="dev-section">
<h2 class="dev-section-title">Choose Your Role</h2>
<div class="role-selector">
<div class="role-card @(_application.Role == ApplicationRole.Tester ? "role-active" : "")"
@onclick="() => SelectRole(ApplicationRole.Tester)">
<div class="role-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
</div>
<h3>Product Tester</h3>
<p>Test our apps across devices, find bugs, and provide feedback that shapes our products.</p>
</div>
<div class="role-card @(_application.Role == ApplicationRole.Developer ? "role-active" : "")"
@onclick="() => SelectRole(ApplicationRole.Developer)">
<div class="role-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</div>
<h3>Developer</h3>
<p>Contribute code, build modules, and help architect privacy-first solutions.</p>
</div>
</div>
<ValidationMessage For="() => _application.Role" />
</div>
<!-- Personal Details -->
<div class="dev-section">
<h2 class="dev-section-title">About You</h2>
<div class="form-grid">
<div class="form-group">
<label for="fullName">Full Name</label>
<InputText id="fullName" @bind-Value="_application.FullName" class="form-input" placeholder="Jane Doe" />
<ValidationMessage For="() => _application.FullName" />
</div>
<div class="form-group">
<label for="email">Email Address</label>
<InputText id="email" @bind-Value="_application.Email" class="form-input" placeholder="jane@example.com" />
<ValidationMessage For="() => _application.Email" />
</div>
<div class="form-group">
<label for="username">Desired Username</label>
<InputText id="username" @bind-Value="_application.DesiredUsername" class="form-input" placeholder="janedoe" />
<span class="form-hint">This will be your handle across SilverLabs services</span>
<ValidationMessage For="() => _application.DesiredUsername" />
</div>
<div class="form-group">
<label for="timezone">Location / Timezone</label>
<InputText id="timezone" @bind-Value="_application.Timezone" class="form-input" placeholder="e.g. Europe/London, US/Eastern" />
<ValidationMessage For="() => _application.Timezone" />
</div>
</div>
</div>
<!-- Platforms -->
<div class="dev-section">
<h2 class="dev-section-title">Devices & Platforms</h2>
<p class="dev-section-desc">Which platforms do you use or have access to?</p>
<div class="platform-grid">
@foreach (var platform in _availablePlatforms)
{
var isChecked = _application.Platforms.Contains(platform);
<label class="platform-chip @(isChecked ? "platform-active" : "")">
<input type="checkbox" checked="@isChecked"
@onchange="() => TogglePlatform(platform)" />
<span>@platform</span>
</label>
}
</div>
<ValidationMessage For="() => _application.Platforms" />
</div>
<!-- Developer-only: Skills -->
@if (_application.Role == ApplicationRole.Developer)
{
<div class="dev-section dev-section-fade-in">
<h2 class="dev-section-title">Skills & Experience</h2>
<p class="dev-section-desc">Tell us about your technical background — languages, frameworks, and any open-source contributions.</p>
<div class="form-group">
<InputTextArea id="skills" @bind-Value="_application.Skills" class="form-input form-textarea"
placeholder="e.g. C#/.NET 5 years, Blazor, PostgreSQL, Docker, contributed to..." rows="5" />
</div>
</div>
}
<!-- Motivation -->
<div class="dev-section">
<h2 class="dev-section-title">Why SilverLabs?</h2>
<p class="dev-section-desc">What draws you to privacy-first development? What do you hope to contribute?</p>
<div class="form-group">
<InputTextArea id="motivation" @bind-Value="_application.Motivation" class="form-input form-textarea"
placeholder="Tell us what motivates you..." rows="5" />
<ValidationMessage For="() => _application.Motivation" />
</div>
</div>
<!-- What You Get -->
<div class="dev-section dev-perks">
<h2 class="dev-section-title">What You'll Get</h2>
<div class="perks-grid">
<div class="perk-item">
<strong>@@@("username")@@silverlabs.uk</strong>
<span>Your own SilverLabs email</span>
</div>
<div class="perk-item">
<strong>SilverDESK</strong>
<span>Project management & issue tracking</span>
</div>
<div class="perk-item">
<strong>Mattermost</strong>
<span>Team chat & collaboration</span>
</div>
<div class="perk-item">
<strong>Gitea Access</strong>
<span>Source code repositories</span>
</div>
</div>
</div>
<!-- Submit -->
<div class="dev-submit-area">
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="dev-error">@_errorMessage</div>
}
<button type="submit" class="dev-btn dev-btn-primary" disabled="@_submitting">
@if (_submitting)
{
<span class="btn-spinner"></span>
<span>Submitting...</span>
}
else
{
<span>Submit Application</span>
}
</button>
</div>
</EditForm>
}
<a href="/" class="back-link">← Back to SilverLabs Home</a>
</div>
</div>
@code {
private DeveloperApplication _application = new() { Role = ApplicationRole.Tester };
private bool _submitting;
private bool _submitted;
private string? _resultMessage;
private string? _errorMessage;
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
private void SelectRole(ApplicationRole role)
{
_application.Role = role;
}
private void TogglePlatform(string platform)
{
if (_application.Platforms.Contains(platform))
_application.Platforms.Remove(platform);
else
_application.Platforms.Add(platform);
}
private async Task HandleSubmit()
{
_errorMessage = null;
_submitting = true;
try
{
var (success, message) = await ApplicationService.SubmitApplicationAsync(_application);
if (success)
{
_resultMessage = message;
_submitted = true;
}
else
{
_errorMessage = message;
}
}
catch
{
_errorMessage = "An unexpected error occurred. Please try again later.";
}
finally
{
_submitting = false;
}
}
}

View File

@@ -12,7 +12,7 @@
<p class="subtitle">Your Innovation Gateway</p>
<div class="gateway-grid">
<a href="https://helpdesk.silverlabs.uk" class="gateway-card">
<a href="https://silverdesk.silverlabs.uk" class="gateway-card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
@@ -57,6 +57,19 @@
<h2 class="card-title">SDK</h2>
<p class="card-description">Developer Resources</p>
</a>
<a href="/developers" class="gateway-card">
<div class="card-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
</div>
<h2 class="card-title">Developers</h2>
<p class="card-description">Join the Team</p>
</a>
</div>
</main>

View File

@@ -227,7 +227,7 @@ curl https://library.silverlabs.uk/api/modules/search?q=auth</code></div>
<h2>💬 Support</h2>
<p>Need help? We're here for you:</p>
<ul>
<li><strong>Help Desk:</strong> <a href="https://helpdesk.silverlabs.uk" target="_blank" style="color: #a78bfa;">helpdesk.silverlabs.uk</a></li>
<li><strong>Help Desk:</strong> <a href="https://silverdesk.silverlabs.uk" target="_blank" style="color: #a78bfa;">silverdesk.silverlabs.uk</a></li>
<li><strong>Issues:</strong> <a href="https://gitlab.silverlabs.uk/silverlabs/silvershell/-/issues" target="_blank" style="color: #a78bfa;">GitLab Issues</a></li>
</ul>
</div>

View File

@@ -0,0 +1,43 @@
using SilverLabs.Website.Models;
using SilverLabs.Website.Services;
namespace SilverLabs.Website.Endpoints;
public static class DeveloperEndpoints
{
public static void MapDeveloperEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/developers");
group.MapPost("/apply", async (DeveloperApplication application, DeveloperApplicationService service) =>
{
var (success, message) = await service.SubmitApplicationAsync(application);
return success
? Results.Ok(new { message })
: Results.Problem(message, statusCode: 502);
});
group.MapPost("/approve/{ticketId:int}", async (
int ticketId,
ApproveRequest request,
ProvisioningService service,
HttpContext context,
IConfiguration config) =>
{
var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
var expectedKey = config["AdminApiKey"];
if (string.IsNullOrEmpty(expectedKey) || apiKey != expectedKey)
return Results.Unauthorized();
var (success, message) = await service.ApproveApplicationAsync(
ticketId, request.Username, request.Email, request.FullName);
return success
? Results.Ok(new { message })
: Results.Problem(message, statusCode: 502);
});
}
}
public record ApproveRequest(string Username, string Email, string FullName);

View File

@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
namespace SilverLabs.Website.Models;
public class DeveloperApplication
{
[Required(ErrorMessage = "Full name is required")]
[StringLength(100, MinimumLength = 2)]
public string FullName { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Username is required")]
[RegularExpression(@"^[a-zA-Z0-9_-]{3,30}$", ErrorMessage = "Username must be 3-30 characters, letters, numbers, hyphens and underscores only")]
public string DesiredUsername { get; set; } = string.Empty;
[Required(ErrorMessage = "Timezone is required")]
public string Timezone { get; set; } = string.Empty;
[Required(ErrorMessage = "Please select a role")]
public ApplicationRole Role { get; set; }
[Required(ErrorMessage = "Please select at least one platform")]
[MinLength(1, ErrorMessage = "Please select at least one platform")]
public List<string> Platforms { get; set; } = new();
public string? Skills { get; set; }
[Required(ErrorMessage = "Please tell us why you want to join")]
[StringLength(2000, MinimumLength = 20, ErrorMessage = "Motivation must be between 20 and 2000 characters")]
public string Motivation { get; set; } = string.Empty;
}
public enum ApplicationRole
{
Tester,
Developer
}

View File

@@ -1,4 +1,6 @@
using SilverLabs.Website.Components;
using SilverLabs.Website.Endpoints;
using SilverLabs.Website.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -6,6 +8,42 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// HttpClient for SilverDESK (used by DeveloperApplicationService directly)
builder.Services.AddHttpClient<DeveloperApplicationService>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["SilverDesk:BaseUrl"] ?? "https://silverdesk.silverlabs.uk");
var apiKey = builder.Configuration["SilverDesk:ApiKey"];
if (!string.IsNullOrEmpty(apiKey))
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
});
// Named HttpClients for provisioning
builder.Services.AddHttpClient("SilverDesk", client =>
{
client.BaseAddress = new Uri(builder.Configuration["SilverDesk:BaseUrl"] ?? "https://silverdesk.silverlabs.uk");
var apiKey = builder.Configuration["SilverDesk:ApiKey"];
if (!string.IsNullOrEmpty(apiKey))
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
});
builder.Services.AddHttpClient("Mattermost", client =>
{
client.BaseAddress = new Uri(builder.Configuration["Mattermost:BaseUrl"] ?? "https://ops.silverlined.uk");
var token = builder.Configuration["Mattermost:ApiToken"];
if (!string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
});
builder.Services.AddHttpClient("Mailcow", client =>
{
client.BaseAddress = new Uri(builder.Configuration["Mailcow:BaseUrl"] ?? "https://mail.silverlined.uk");
var apiKey = builder.Configuration["Mailcow:ApiKey"];
if (!string.IsNullOrEmpty(apiKey))
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
});
builder.Services.AddScoped<ProvisioningService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -24,4 +62,6 @@ app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapDeveloperEndpoints();
app.Run();

View File

@@ -0,0 +1,80 @@
using System.Text;
using System.Text.Json;
using SilverLabs.Website.Models;
namespace SilverLabs.Website.Services;
public class DeveloperApplicationService
{
private readonly HttpClient _httpClient;
private readonly ILogger<DeveloperApplicationService> _logger;
public DeveloperApplicationService(HttpClient httpClient, ILogger<DeveloperApplicationService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<(bool Success, string Message)> SubmitApplicationAsync(DeveloperApplication application)
{
try
{
var ticketBody = FormatTicketBody(application);
var payload = new
{
Subject = $"[Developer Program] {application.Role} Application - {application.FullName}",
Description = ticketBody,
Priority = "Medium",
Category = "Developer Program"
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/api/tickets", content);
if (response.IsSuccessStatusCode)
{
_logger.LogInformation("Developer application submitted for {Email} as {Role}", application.Email, application.Role);
return (true, "Application submitted successfully! We'll review it and get back to you soon.");
}
var errorBody = await response.Content.ReadAsStringAsync();
_logger.LogError("Failed to create ticket: {StatusCode} - {Body}", response.StatusCode, errorBody);
return (false, "Something went wrong submitting your application. Please try again later.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting developer application for {Email}", application.Email);
return (false, "Unable to connect to the application service. Please try again later.");
}
}
private static string FormatTicketBody(DeveloperApplication app)
{
var sb = new StringBuilder();
sb.AppendLine("## Developer Program Application");
sb.AppendLine();
sb.AppendLine($"**Role:** {app.Role}");
sb.AppendLine($"**Full Name:** {app.FullName}");
sb.AppendLine($"**Email:** {app.Email}");
sb.AppendLine($"**Desired Username:** {app.DesiredUsername}");
sb.AppendLine($"**Timezone:** {app.Timezone}");
sb.AppendLine();
sb.AppendLine($"**Platforms:** {string.Join(", ", app.Platforms)}");
sb.AppendLine();
if (app.Role == ApplicationRole.Developer && !string.IsNullOrWhiteSpace(app.Skills))
{
sb.AppendLine("**Skills & Experience:**");
sb.AppendLine(app.Skills);
sb.AppendLine();
}
sb.AppendLine("**Motivation:**");
sb.AppendLine(app.Motivation);
return sb.ToString();
}
}

View File

@@ -0,0 +1,156 @@
using System.Text;
using System.Text.Json;
namespace SilverLabs.Website.Services;
public class ProvisioningService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ProvisioningService> _logger;
public ProvisioningService(IHttpClientFactory httpClientFactory, ILogger<ProvisioningService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task<(bool Success, string Message)> ApproveApplicationAsync(
int ticketId, string username, string email, string fullName)
{
var results = new List<string>();
var allSuccess = true;
// 1. Create SilverDESK user
var (deskOk, deskMsg) = await CreateSilverDeskUserAsync(username, email, fullName);
results.Add($"SilverDESK: {deskMsg}");
if (!deskOk) allSuccess = false;
// 2. Create Mattermost user
var (mmOk, mmMsg) = await CreateMattermostUserAsync(username, email, fullName);
results.Add($"Mattermost: {mmMsg}");
if (!mmOk) allSuccess = false;
// 3. Create Mailcow mailbox
var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName);
results.Add($"Mailcow: {mailMsg}");
if (!mailOk) allSuccess = false;
// 4. Update SilverDESK ticket
if (allSuccess)
{
await UpdateTicketStatusAsync(ticketId, "approved", string.Join("\n", results));
}
var summary = string.Join("; ", results);
_logger.LogInformation("Provisioning for {Username}: {Summary}", username, summary);
return (allSuccess, summary);
}
public async Task<(bool Success, string Message)> CreateSilverDeskUserAsync(string username, string email, string fullName)
{
try
{
var client = _httpClientFactory.CreateClient("SilverDesk");
var payload = new { username, email, name = fullName, role = "user" };
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/users", content);
if (response.IsSuccessStatusCode)
return (true, "User created");
var body = await response.Content.ReadAsStringAsync();
_logger.LogError("SilverDESK user creation failed: {Status} {Body}", response.StatusCode, body);
return (false, $"Failed ({response.StatusCode})");
}
catch (Exception ex)
{
_logger.LogError(ex, "SilverDESK user creation error for {Username}", username);
return (false, $"Error: {ex.Message}");
}
}
public async Task<(bool Success, string Message)> CreateMattermostUserAsync(string username, string email, string fullName)
{
try
{
var client = _httpClientFactory.CreateClient("Mattermost");
var nameParts = fullName.Split(' ', 2);
var payload = new
{
email,
username,
first_name = nameParts[0],
last_name = nameParts.Length > 1 ? nameParts[1] : "",
password = Guid.NewGuid().ToString("N")[..16] + "!A1" // Temporary password
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/v4/users", content);
if (response.IsSuccessStatusCode)
return (true, "User created");
var body = await response.Content.ReadAsStringAsync();
_logger.LogError("Mattermost user creation failed: {Status} {Body}", response.StatusCode, body);
return (false, $"Failed ({response.StatusCode})");
}
catch (Exception ex)
{
_logger.LogError(ex, "Mattermost user creation error for {Username}", username);
return (false, $"Error: {ex.Message}");
}
}
public async Task<(bool Success, string Message)> CreateMailcowMailboxAsync(string username, string fullName)
{
try
{
var client = _httpClientFactory.CreateClient("Mailcow");
var payload = new
{
local_part = username,
domain = "silverlabs.uk",
name = fullName,
password = Guid.NewGuid().ToString("N")[..16] + "!A1", // Temporary password
password2 = "",
quota = 1024, // 1GB
active = 1,
force_pw_update = 1
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/v1/add/mailbox", content);
if (response.IsSuccessStatusCode)
return (true, "Mailbox created");
var body = await response.Content.ReadAsStringAsync();
_logger.LogError("Mailcow mailbox creation failed: {Status} {Body}", response.StatusCode, body);
return (false, $"Failed ({response.StatusCode})");
}
catch (Exception ex)
{
_logger.LogError(ex, "Mailcow mailbox creation error for {Username}", username);
return (false, $"Error: {ex.Message}");
}
}
private async Task UpdateTicketStatusAsync(int ticketId, string status, string note)
{
try
{
var client = _httpClientFactory.CreateClient("SilverDesk");
var payload = new { status, note = $"Application approved. Provisioning results:\n{note}" };
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
await client.PutAsync($"/api/tickets/{ticketId}", content);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update ticket {TicketId} status", ticketId);
}
}
}

View File

@@ -5,5 +5,18 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"SilverDesk": {
"BaseUrl": "https://silverdesk.silverlabs.uk",
"ApiKey": "silverdesk-mcp-2025-secure-key"
},
"Mattermost": {
"BaseUrl": "https://ops.silverlined.uk",
"ApiToken": "ktmfkpxz7ffr5g1imuqg8hm58c"
},
"Mailcow": {
"BaseUrl": "https://mail.silverlined.uk",
"ApiKey": ""
},
"AdminApiKey": ""
}

View File

@@ -0,0 +1,423 @@
/* Developers Page Styles */
.dev-container {
max-width: 860px;
margin: 0 auto;
padding: 0 2rem 4rem;
}
.dev-header {
text-align: center;
margin-bottom: 3rem;
}
.dev-header h1 {
font-size: 2.8rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, #4DD0E1 0%, #00B8D4 40%, #1E5A9E 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dev-subtitle {
font-size: 1.15rem;
color: rgba(255, 255, 255, 0.7);
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
/* Sections */
.dev-section {
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2rem;
margin-bottom: 1.5rem;
animation: devFadeIn 0.5s ease both;
}
.dev-section:nth-child(2) { animation-delay: 0.1s; }
.dev-section:nth-child(3) { animation-delay: 0.15s; }
.dev-section:nth-child(4) { animation-delay: 0.2s; }
.dev-section:nth-child(5) { animation-delay: 0.25s; }
.dev-section:nth-child(6) { animation-delay: 0.3s; }
@keyframes devFadeIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.dev-section-fade-in {
animation: devFadeIn 0.4s ease both !important;
}
.dev-section-title {
font-size: 1.4rem;
margin-bottom: 0.75rem;
color: #4DD0E1;
}
.dev-section-desc {
color: rgba(255, 255, 255, 0.6);
margin-bottom: 1rem;
line-height: 1.5;
}
/* Role Selector */
.role-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.role-card {
background: rgba(255, 255, 255, 0.04);
border: 2px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.25s ease;
text-align: center;
}
.role-card:hover {
border-color: rgba(77, 208, 225, 0.4);
background: rgba(77, 208, 225, 0.06);
}
.role-card.role-active {
border-color: #4DD0E1;
background: rgba(77, 208, 225, 0.1);
box-shadow: 0 0 24px rgba(77, 208, 225, 0.15);
}
.role-icon {
width: 56px;
height: 56px;
margin: 0 auto 0.75rem;
background: rgba(255, 255, 255, 0.08);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s ease;
}
.role-active .role-icon {
background: rgba(77, 208, 225, 0.18);
}
.role-icon svg {
width: 28px;
height: 28px;
stroke: rgba(255, 255, 255, 0.6);
transition: stroke 0.25s ease;
}
.role-active .role-icon svg {
stroke: #4DD0E1;
}
.role-card h3 {
font-size: 1.2rem;
margin-bottom: 0.4rem;
color: #fff;
}
.role-card p {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.55);
line-height: 1.4;
}
/* Form Elements */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-size: 0.85rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 0.4rem;
letter-spacing: 0.02em;
}
.form-input {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
padding: 0.75rem 1rem;
color: #fff;
font-size: 0.95rem;
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
outline: none;
width: 100%;
}
.form-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.form-input:focus {
border-color: #4DD0E1;
box-shadow: 0 0 0 3px rgba(77, 208, 225, 0.12);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-hint {
font-size: 0.78rem;
color: rgba(255, 255, 255, 0.4);
margin-top: 0.3rem;
}
/* Validation messages */
.validation-message {
color: #f87171;
font-size: 0.8rem;
margin-top: 0.3rem;
}
/* Platform Chips */
.platform-grid {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
.platform-chip {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 100px;
cursor: pointer;
transition: all 0.2s ease;
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
user-select: none;
}
.platform-chip input[type="checkbox"] {
display: none;
}
.platform-chip:hover {
border-color: rgba(77, 208, 225, 0.4);
}
.platform-chip.platform-active {
background: rgba(77, 208, 225, 0.12);
border-color: #4DD0E1;
color: #4DD0E1;
}
/* Perks */
.dev-perks {
background: linear-gradient(135deg, rgba(30, 90, 158, 0.15) 0%, rgba(0, 184, 212, 0.08) 100%);
border-color: rgba(77, 208, 225, 0.2);
}
.perks-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.perk-item {
display: flex;
flex-direction: column;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.04);
border-radius: 10px;
}
.perk-item strong {
color: #4DD0E1;
font-size: 0.95rem;
margin-bottom: 0.2rem;
}
.perk-item span {
color: rgba(255, 255, 255, 0.55);
font-size: 0.82rem;
}
/* Submit Area */
.dev-submit-area {
text-align: center;
margin-top: 1.5rem;
margin-bottom: 2rem;
}
.dev-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.85rem 2.5rem;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.25s ease;
border: none;
font-family: inherit;
}
.dev-btn-primary {
background: linear-gradient(135deg, #1E5A9E 0%, #00B8D4 100%);
color: #fff;
box-shadow: 0 4px 20px rgba(0, 184, 212, 0.25);
}
.dev-btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 184, 212, 0.35);
}
.dev-btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.dev-btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dev-btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
}
.btn-spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
/* Error / Success */
.dev-error {
background: rgba(248, 113, 113, 0.1);
border: 1px solid rgba(248, 113, 113, 0.3);
color: #f87171;
padding: 0.75rem 1rem;
border-radius: 10px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.dev-success-panel {
text-align: center;
padding: 4rem 2rem;
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(12px);
border: 1px solid rgba(77, 208, 225, 0.25);
border-radius: 20px;
margin-bottom: 2rem;
animation: devFadeIn 0.5s ease both;
}
.success-icon {
width: 72px;
height: 72px;
margin: 0 auto 1.5rem;
background: rgba(77, 208, 225, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.success-icon svg {
width: 36px;
height: 36px;
stroke: #4DD0E1;
}
.dev-success-panel h2 {
font-size: 1.8rem;
margin-bottom: 0.75rem;
color: #4DD0E1;
}
.dev-success-panel p {
color: rgba(255, 255, 255, 0.7);
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
line-height: 1.5;
}
/* Back Link - reuse from sdk-styles */
.dev-container .back-link {
display: inline-block;
margin-top: 1rem;
color: #4DD0E1;
text-decoration: none;
font-size: 1rem;
transition: color 0.2s ease;
}
.dev-container .back-link:hover {
color: #00B8D4;
}
/* Responsive */
@media (max-width: 768px) {
.dev-header h1 {
font-size: 2rem;
}
.role-selector {
grid-template-columns: 1fr;
}
.form-grid {
grid-template-columns: 1fr;
}
.perks-grid {
grid-template-columns: 1fr;
}
.dev-container {
padding: 0 1rem 3rem;
}
.dev-section {
padding: 1.5rem;
}
}
@media (max-width: 480px) {
.dev-header h1 {
font-size: 1.6rem;
}
.dev-subtitle {
font-size: 1rem;
}
}