Merge remote-tracking branch 'origin/main' into unified-sign-tool
This commit is contained in:
@@ -217,6 +217,39 @@ jobs:
|
||||
path: frontend/dist/
|
||||
retention-days: 3
|
||||
|
||||
playwright-e2e:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
- name: Generate icons
|
||||
run: cd frontend && node scripts/generate-icons.js
|
||||
- name: Install Playwright (chromium only)
|
||||
run: cd frontend && npx playwright install chromium --with-deps
|
||||
- name: Run E2E tests (chromium)
|
||||
run: cd frontend && npx playwright test --project=chromium
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: playwright-report-pr-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build]
|
||||
@@ -358,6 +391,7 @@ jobs:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DOCKER_BASE_CHANGED: ${{ needs.files-changed.outputs.docker-base }}
|
||||
|
||||
- name: Upload Cucumber Report
|
||||
if: always()
|
||||
@@ -368,6 +402,15 @@ jobs:
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: docker-compose-test-reports
|
||||
path: testing/reports/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Cucumber Test Report
|
||||
if: always()
|
||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
uses: crazy-max/ghaction-github-labeler@548a7c3603594ec17c819e1239f281a3b801ab4d # v6.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
yaml-file: .github/labels.yml
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Nightly E2E Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * *" # 2 AM UTC every night
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
playwright-all-browsers:
|
||||
name: Playwright (chromium + firefox + webkit)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
|
||||
- name: Generate icons
|
||||
run: cd frontend && node scripts/generate-icons.js
|
||||
|
||||
- name: Install all Playwright browsers
|
||||
run: cd frontend && npx playwright install --with-deps
|
||||
|
||||
- name: Run E2E tests (all browsers)
|
||||
run: cd frontend && npx playwright test
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: playwright-nightly-${{ github.run_id }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 14
|
||||
@@ -203,6 +203,9 @@ out/
|
||||
*.jks
|
||||
*.asc
|
||||
|
||||
# Allow test fixture certificates (synthetic, no real credentials)
|
||||
!frontend/src/core/tests/test-fixtures/certs/**
|
||||
|
||||
# SSH Keys
|
||||
*.pub
|
||||
*.priv
|
||||
|
||||
@@ -20,16 +20,12 @@ This file provides guidance to AI Agents when working with code in this reposito
|
||||
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
|
||||
|
||||
### Python Development
|
||||
Development for the AI engine happens in the `engine/` folder. It's built with Langchain and Pydantic and allows for the creation and editing of PDF documents. The frontend calls the Python via Java as a proxy.
|
||||
Development for the AI engine happens in the `engine/` folder. The frontend calls the Python via Java as a proxy.
|
||||
|
||||
- Python version is 3.13; use modern Python features (type aliases, pattern matching, dataclasses, etc.) where they help clarity.
|
||||
- Write fully type-correct code; keep pyright clean and avoid `Any` unless strictly necessary.
|
||||
- JSON handling: deserialize into fully typed Pydantic models as early as possible, and serialize back from Pydantic models as late as possible.
|
||||
- Follow the engine-specific guidance in [engine/AGENTS.md](engine/AGENTS.md) for Python architecture, code style, and AI usage.
|
||||
- Use Makefile commands for Python work:
|
||||
- From `engine/`: `make check` to lint, type-check, test, etc. and `make fix` to fix easily fixable linting & formatting issues.
|
||||
- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed appropriately there, followed by running `make install`.
|
||||
- Prefer using classes to nesting functions, and make other similar architectural decisions to improve testability. Do not nest classes or functions unless specifically required to for the code construct (like a decorator).
|
||||
- All environment variables used within the code must begin with the `STIRLING_` prefix in order to keep them unique and easier to find.
|
||||
- From `engine/`: `make check` to lint, type-check, test, etc. and `make fix` to fix easily fixable linting and formatting issues.
|
||||
- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `make install`.
|
||||
|
||||
### Frontend Development
|
||||
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
|
||||
|
||||
@@ -180,7 +180,9 @@ public class RequestUriUtils {
|
||||
|| trimmedUri.startsWith("/readiness")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth)
|
||||
|| trimmedUri.startsWith("/v1/api-docs");
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
// Workflow participant endpoints — access controlled by share tokens, not login
|
||||
|| trimmedUri.startsWith("/api/v1/workflow/participant/");
|
||||
}
|
||||
|
||||
private static String stripContextPath(String contextPath, String requestURI) {
|
||||
|
||||
@@ -171,6 +171,9 @@ out/
|
||||
*.jks
|
||||
*.asc
|
||||
|
||||
# Allow test fixture certificates (synthetic, no real credentials)
|
||||
!src/test/resources/test-certs/**
|
||||
|
||||
# SSH Keys
|
||||
*.pub
|
||||
*.priv
|
||||
|
||||
+85
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.workflow.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
@@ -14,7 +15,9 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -32,9 +35,12 @@ import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.workflow.dto.CertificateInfo;
|
||||
import stirling.software.proprietary.workflow.dto.CertificateValidationResponse;
|
||||
import stirling.software.proprietary.workflow.dto.ParticipantRequest;
|
||||
import stirling.software.proprietary.workflow.dto.WorkflowCreationRequest;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowSession;
|
||||
import stirling.software.proprietary.workflow.service.CertificateSubmissionValidator;
|
||||
import stirling.software.proprietary.workflow.service.SigningFinalizationService;
|
||||
import stirling.software.proprietary.workflow.service.WorkflowSessionService;
|
||||
|
||||
@@ -48,6 +54,7 @@ public class SigningSessionController {
|
||||
private final WorkflowSessionService workflowSessionService;
|
||||
private final UserService userService;
|
||||
private final SigningFinalizationService signingFinalizationService;
|
||||
private final CertificateSubmissionValidator certificateSubmissionValidator;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Operation(summary = "List all signing sessions for current user")
|
||||
@@ -397,6 +404,84 @@ public class SigningSessionController {
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Pre-validate a certificate before signing",
|
||||
description =
|
||||
"Validates that the provided certificate is loadable, not expired, and can "
|
||||
+ "successfully sign a document. Returns validation details so the "
|
||||
+ "user can confirm the correct certificate before committing.")
|
||||
@PostMapping(
|
||||
value = "/cert-sign/validate-certificate",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<CertificateValidationResponse> validateCertificate(
|
||||
@RequestParam("certType") String certType,
|
||||
@RequestParam(value = "password", required = false) String password,
|
||||
@RequestParam(value = "p12File", required = false) MultipartFile p12File,
|
||||
@RequestParam(value = "jksFile", required = false) MultipartFile jksFile,
|
||||
Principal principal) {
|
||||
|
||||
workflowSessionService.ensureSigningEnabled();
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
if (!"SERVER".equalsIgnoreCase(certType)
|
||||
&& !"USER_CERT".equalsIgnoreCase(certType)
|
||||
&& (p12File == null || p12File.isEmpty())
|
||||
&& (jksFile == null || jksFile.isEmpty())) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "No certificate file provided");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] keystoreBytes = null;
|
||||
if (p12File != null && !p12File.isEmpty()) {
|
||||
keystoreBytes = p12File.getBytes();
|
||||
} else if (jksFile != null && !jksFile.isEmpty()) {
|
||||
keystoreBytes = jksFile.getBytes();
|
||||
}
|
||||
|
||||
CertificateInfo info =
|
||||
certificateSubmissionValidator.validateAndExtractInfo(
|
||||
keystoreBytes, certType, password);
|
||||
|
||||
if (info == null) {
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
true, null, null, null, null, false, null));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
true,
|
||||
info.subjectName(),
|
||||
info.issuerName(),
|
||||
info.notAfter() != null ? info.notAfter().toInstant().toString() : null,
|
||||
info.notBefore() != null
|
||||
? info.notBefore().toInstant().toString()
|
||||
: null,
|
||||
info.selfSigned(),
|
||||
null));
|
||||
|
||||
} catch (ResponseStatusException e) {
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
false, null, null, null, null, false, e.getReason()));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading certificate file during pre-validation", e);
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"Failed to read certificate file"));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HELPER METHODS =====
|
||||
|
||||
private User getCurrentUser(Principal principal) {
|
||||
|
||||
+114
@@ -2,6 +2,8 @@ package stirling.software.proprietary.workflow.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -27,6 +30,8 @@ import jakarta.validation.constraints.Size;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.workflow.dto.CertificateInfo;
|
||||
import stirling.software.proprietary.workflow.dto.CertificateValidationResponse;
|
||||
import stirling.software.proprietary.workflow.dto.ParticipantResponse;
|
||||
import stirling.software.proprietary.workflow.dto.SignatureSubmissionRequest;
|
||||
import stirling.software.proprietary.workflow.dto.WetSignatureMetadata;
|
||||
@@ -35,6 +40,7 @@ import stirling.software.proprietary.workflow.model.ParticipantStatus;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowParticipant;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowSession;
|
||||
import stirling.software.proprietary.workflow.repository.WorkflowParticipantRepository;
|
||||
import stirling.software.proprietary.workflow.service.CertificateSubmissionValidator;
|
||||
import stirling.software.proprietary.workflow.service.MetadataEncryptionService;
|
||||
import stirling.software.proprietary.workflow.service.WorkflowSessionService;
|
||||
import stirling.software.proprietary.workflow.util.WorkflowMapper;
|
||||
@@ -59,6 +65,10 @@ public class WorkflowParticipantController {
|
||||
private final WorkflowParticipantRepository participantRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MetadataEncryptionService metadataEncryptionService;
|
||||
private final CertificateSubmissionValidator certificateSubmissionValidator;
|
||||
|
||||
private static final DateTimeFormatter ISO_UTC =
|
||||
DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);
|
||||
|
||||
@Operation(
|
||||
summary = "Get workflow session details by participant token",
|
||||
@@ -173,6 +183,8 @@ public class WorkflowParticipantController {
|
||||
|
||||
return ResponseEntity.ok(WorkflowMapper.toParticipantResponse(participant));
|
||||
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Error submitting signature for participant {}", participant.getEmail(), e);
|
||||
throw new ResponseStatusException(
|
||||
@@ -268,6 +280,94 @@ public class WorkflowParticipantController {
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Pre-validate a certificate before submission",
|
||||
description =
|
||||
"Validates that the provided certificate is loadable, not expired, and can "
|
||||
+ "successfully sign a document. Returns validation details so the "
|
||||
+ "participant can confirm the correct certificate before committing.")
|
||||
@PostMapping(
|
||||
value = "/validate-certificate",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<CertificateValidationResponse> validateCertificate(
|
||||
@RequestParam("participantToken") @NotBlank String participantToken,
|
||||
@RequestParam("certType") String certType,
|
||||
@RequestParam(value = "password", required = false) String password,
|
||||
@RequestParam(value = "p12File", required = false) MultipartFile p12File,
|
||||
@RequestParam(value = "jksFile", required = false) MultipartFile jksFile) {
|
||||
|
||||
workflowSessionService.ensureSigningEnabled();
|
||||
|
||||
participantRepository
|
||||
.findByShareToken(participantToken)
|
||||
.filter(p -> !p.isExpired())
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new ResponseStatusException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"Invalid or expired participant token"));
|
||||
|
||||
// Require a file for non-SERVER/non-USER_CERT types — this is a request error, not a
|
||||
// validation failure
|
||||
if (!"SERVER".equalsIgnoreCase(certType)
|
||||
&& !"USER_CERT".equalsIgnoreCase(certType)
|
||||
&& (p12File == null || p12File.isEmpty())
|
||||
&& (jksFile == null || jksFile.isEmpty())) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "No certificate file provided");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] keystoreBytes = null;
|
||||
if (p12File != null && !p12File.isEmpty()) {
|
||||
keystoreBytes = p12File.getBytes();
|
||||
} else if (jksFile != null && !jksFile.isEmpty()) {
|
||||
keystoreBytes = jksFile.getBytes();
|
||||
}
|
||||
|
||||
CertificateInfo info =
|
||||
certificateSubmissionValidator.validateAndExtractInfo(
|
||||
keystoreBytes, certType, password);
|
||||
|
||||
if (info == null) {
|
||||
// SERVER type — nothing to validate
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
true, null, null, null, null, false, null));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
true,
|
||||
info.subjectName(),
|
||||
info.issuerName(),
|
||||
info.notAfter() != null ? info.notAfter().toInstant().toString() : null,
|
||||
info.notBefore() != null
|
||||
? info.notBefore().toInstant().toString()
|
||||
: null,
|
||||
info.selfSigned(),
|
||||
null));
|
||||
|
||||
} catch (ResponseStatusException e) {
|
||||
// Validation failure — return 200 with valid:false so the frontend can display inline
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
false, null, null, null, null, false, e.getReason()));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading certificate file during pre-validation", e);
|
||||
return ResponseEntity.ok(
|
||||
new CertificateValidationResponse(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"Failed to read certificate file"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds metadata map from signature submission request. Includes certificate submission and
|
||||
* wet signature data.
|
||||
@@ -276,6 +376,20 @@ public class WorkflowParticipantController {
|
||||
throws IOException {
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
|
||||
// Validate certificate before storing — throws 400 if invalid, expired, or wrong password
|
||||
if (request.getCertType() != null && !"SERVER".equalsIgnoreCase(request.getCertType())) {
|
||||
byte[] keystoreBytes = null;
|
||||
if (request.getP12File() != null && !request.getP12File().isEmpty()) {
|
||||
keystoreBytes = request.getP12File().getBytes();
|
||||
} else if (request.getJksFile() != null && !request.getJksFile().isEmpty()) {
|
||||
keystoreBytes = request.getJksFile().getBytes();
|
||||
}
|
||||
if (keystoreBytes != null) {
|
||||
certificateSubmissionValidator.validateAndExtractInfo(
|
||||
keystoreBytes, request.getCertType(), request.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
// Add certificate submission if provided
|
||||
if (request.getCertType() != null) {
|
||||
Map<String, Object> certSubmission = new HashMap<>();
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package stirling.software.proprietary.workflow.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Certificate metadata extracted from a keystore submission. Returned by
|
||||
* CertificateSubmissionValidator after successful validation so callers can surface details
|
||||
* (expiry, subject) to the user.
|
||||
*/
|
||||
public record CertificateInfo(
|
||||
String subjectName, String issuerName, Date notBefore, Date notAfter, boolean selfSigned) {}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package stirling.software.proprietary.workflow.dto;
|
||||
|
||||
/**
|
||||
* API response returned by the certificate pre-validation endpoints. Always returns HTTP 200; the
|
||||
* {@code valid} field indicates success. Frontend should use this to display inline feedback before
|
||||
* the user completes signing.
|
||||
*/
|
||||
public record CertificateValidationResponse(
|
||||
boolean valid,
|
||||
String subjectName,
|
||||
String issuerName,
|
||||
/** ISO-8601 formatted expiry date, or null if validation failed. */
|
||||
String notAfter,
|
||||
/** ISO-8601 formatted start-of-validity date, or null if validation failed. */
|
||||
String notBefore,
|
||||
boolean selfSigned,
|
||||
/** Human-readable error message, or null if valid. */
|
||||
String error) {}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package stirling.software.proprietary.workflow.service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.service.PdfSigningService;
|
||||
import stirling.software.proprietary.workflow.dto.CertificateInfo;
|
||||
|
||||
/**
|
||||
* Validates a certificate submission before it is stored in participant metadata. Catches issues
|
||||
* (wrong password, expired cert, algorithm incompatibility) at signing time rather than days later
|
||||
* at finalization.
|
||||
*
|
||||
* <p>The core check is a test-sign of a minimal blank PDF using the exact same {@link
|
||||
* PdfSigningService} code path used at finalization, so any failure that would block finalization
|
||||
* is caught here first.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CertificateSubmissionValidator {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.systemDefault());
|
||||
|
||||
private final PdfSigningService pdfSigningService;
|
||||
|
||||
/**
|
||||
* Validates a certificate submission end-to-end by:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Loading the keystore with the provided password
|
||||
* <li>Checking certificate validity (expiry, not-yet-valid)
|
||||
* <li>Test-signing a blank PDF to confirm the key and certificate are fully functional
|
||||
* </ol>
|
||||
*
|
||||
* @param keystoreBytes raw bytes of the keystore file
|
||||
* @param certType "P12", "PKCS12", "PFX", or "JKS" (case-insensitive)
|
||||
* @param password keystore password (may be null or empty)
|
||||
* @return {@link CertificateInfo} with subject, issuer, and validity dates on success
|
||||
* @throws ResponseStatusException HTTP 400 with a user-friendly message on any failure
|
||||
*/
|
||||
public CertificateInfo validateAndExtractInfo(
|
||||
byte[] keystoreBytes, String certType, String password) {
|
||||
if (certType == null
|
||||
|| "SERVER".equalsIgnoreCase(certType)
|
||||
|| "USER_CERT".equalsIgnoreCase(certType)) {
|
||||
// Server-managed or pre-configured user certificate — no file uploaded, nothing to
|
||||
// validate
|
||||
return null;
|
||||
}
|
||||
|
||||
char[] passwordChars = password != null ? password.toCharArray() : new char[0];
|
||||
|
||||
KeyStore keystore = loadKeyStore(keystoreBytes, certType, passwordChars);
|
||||
X509Certificate cert = extractSigningCert(keystore, passwordChars);
|
||||
|
||||
validateCertValidity(cert);
|
||||
|
||||
String subjectName = extractCN(cert.getSubjectX500Principal().getName());
|
||||
String issuerName = extractCN(cert.getIssuerX500Principal().getName());
|
||||
boolean selfSigned = cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
|
||||
|
||||
testSign(keystore, passwordChars, subjectName);
|
||||
|
||||
return new CertificateInfo(
|
||||
subjectName, issuerName, cert.getNotBefore(), cert.getNotAfter(), selfSigned);
|
||||
}
|
||||
|
||||
// ---- private helpers ----
|
||||
|
||||
private KeyStore loadKeyStore(byte[] bytes, String certType, char[] password) {
|
||||
String keystoreType = resolveKeystoreType(certType);
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance(keystoreType);
|
||||
ks.load(new java.io.ByteArrayInputStream(bytes), password);
|
||||
return ks;
|
||||
} catch (IOException e) {
|
||||
// PKCS12: wrong password produces an IOException with "keystore password was incorrect"
|
||||
// JKS: wrong password produces IOException wrapping UnrecoverableKeyException
|
||||
log.debug("Failed to load {} keystore: {}", keystoreType, e.getMessage());
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Invalid certificate password or corrupt keystore file");
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to instantiate {} keystore: {}", keystoreType, e.getMessage());
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Invalid certificate password or corrupt keystore file");
|
||||
}
|
||||
}
|
||||
|
||||
private X509Certificate extractSigningCert(KeyStore keystore, char[] password) {
|
||||
try {
|
||||
Enumeration<String> aliases = keystore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
PrivateKey key = null;
|
||||
try {
|
||||
key = (PrivateKey) keystore.getKey(alias, password);
|
||||
} catch (UnrecoverableKeyException | java.security.NoSuchAlgorithmException e) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Invalid certificate password or corrupt keystore file");
|
||||
}
|
||||
if (key == null) continue;
|
||||
|
||||
Certificate[] chain = keystore.getCertificateChain(alias);
|
||||
if (chain != null && chain.length > 0 && chain[0] instanceof X509Certificate) {
|
||||
return (X509Certificate) chain[0];
|
||||
}
|
||||
}
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (KeyStoreException e) {
|
||||
log.debug("KeyStore alias enumeration failed: {}", e.getMessage());
|
||||
}
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "No private key found in the provided keystore");
|
||||
}
|
||||
|
||||
private void validateCertValidity(X509Certificate cert) {
|
||||
try {
|
||||
cert.checkValidity();
|
||||
} catch (CertificateExpiredException e) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Certificate has expired (expired: "
|
||||
+ DATE_FORMAT.format(cert.getNotAfter().toInstant())
|
||||
+ ")");
|
||||
} catch (CertificateNotYetValidException e) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Certificate is not yet valid (valid from: "
|
||||
+ DATE_FORMAT.format(cert.getNotBefore().toInstant())
|
||||
+ ")");
|
||||
}
|
||||
}
|
||||
|
||||
private void testSign(KeyStore keystore, char[] password, String signerName) {
|
||||
try {
|
||||
byte[] blankPdf = createBlankPdf();
|
||||
pdfSigningService.signWithKeystore(
|
||||
blankPdf, keystore, password, false, null, signerName, null, null, false);
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.debug("Certificate test-sign failed: {}", e.getMessage());
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Certificate is not compatible with the signing algorithm: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a minimal valid 1-page blank PDF for use in test-signing. */
|
||||
private byte[] createBlankPdf() throws IOException {
|
||||
try (PDDocument doc = new PDDocument();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
doc.addPage(new PDPage());
|
||||
doc.save(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the user-facing certType string to a JCA KeyStore type string.
|
||||
*
|
||||
* <p>All PKCS12 variants ("P12", "PKCS12", "PFX") map to {@code "PKCS12"}. {@code "JKS"} maps
|
||||
* to {@code "JKS"}.
|
||||
*/
|
||||
private String resolveKeystoreType(String certType) {
|
||||
if (certType == null) return "PKCS12";
|
||||
return switch (certType.toUpperCase()) {
|
||||
case "JKS" -> "JKS";
|
||||
default -> "PKCS12"; // P12, PKCS12, PFX
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the CN value from an X.500 distinguished name string. Falls back to the full DN if
|
||||
* no CN attribute is present.
|
||||
*/
|
||||
private String extractCN(String dn) {
|
||||
if (dn == null) return "";
|
||||
for (String part : dn.split(",")) {
|
||||
String trimmed = part.trim();
|
||||
if (trimmed.toUpperCase().startsWith("CN=")) {
|
||||
return trimmed.substring(3);
|
||||
}
|
||||
}
|
||||
return dn;
|
||||
}
|
||||
}
|
||||
+2
@@ -822,6 +822,8 @@ public class SigningFinalizationService {
|
||||
|
||||
switch (certType) {
|
||||
case "P12":
|
||||
case "PKCS12":
|
||||
case "PFX":
|
||||
if (submission.getP12Keystore() == null) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "P12 keystore data is required");
|
||||
|
||||
+22
-1
@@ -61,6 +61,7 @@ public class WorkflowSessionService {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final MetadataEncryptionService metadataEncryptionService;
|
||||
private final CertificateSubmissionValidator certificateSubmissionValidator;
|
||||
|
||||
public void ensureSigningEnabled() {
|
||||
if (!applicationProperties.getStorage().isEnabled()
|
||||
@@ -684,7 +685,27 @@ public class WorkflowSessionService {
|
||||
metadata = new HashMap<>(existingMetadata);
|
||||
}
|
||||
|
||||
// 1. Store certificate submission data
|
||||
// 1. Validate certificate before storing — throws 400 if invalid, expired, or wrong
|
||||
// password
|
||||
if (request.getCertType() != null
|
||||
&& !"SERVER".equalsIgnoreCase(request.getCertType())
|
||||
&& request.getP12File() != null
|
||||
&& !request.getP12File().isEmpty()) {
|
||||
try {
|
||||
certificateSubmissionValidator.validateAndExtractInfo(
|
||||
request.getP12File().getBytes(),
|
||||
request.getCertType(),
|
||||
request.getPassword());
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read P12 keystore file for validation", e);
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "Failed to process certificate file");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Store certificate submission data
|
||||
Map<String, Object> certSubmission = new HashMap<>();
|
||||
certSubmission.put("certType", request.getCertType());
|
||||
certSubmission.put("password", metadataEncryptionService.encrypt(request.getPassword()));
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package stirling.software.proprietary.workflow.controller;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import stirling.software.common.service.PdfSigningService;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowParticipant;
|
||||
import stirling.software.proprietary.workflow.repository.WorkflowParticipantRepository;
|
||||
import stirling.software.proprietary.workflow.service.CertificateSubmissionValidator;
|
||||
import stirling.software.proprietary.workflow.service.MetadataEncryptionService;
|
||||
import stirling.software.proprietary.workflow.service.WorkflowSessionService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Integration test that wires the real {@link CertificateSubmissionValidator} into {@link
|
||||
* WorkflowParticipantController} and exercises it with the actual test-certificate files.
|
||||
*
|
||||
* <p>This fills the gap between the two unit-test layers:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link WorkflowParticipantValidateCertificateTest} — controller only, validator mocked
|
||||
* <li>{@link stirling.software.proprietary.workflow.service.CertificateSubmissionValidatorTest} —
|
||||
* validator only, certs generated programmatically
|
||||
* </ul>
|
||||
*
|
||||
* Here both layers run together against real .p12 / .jks files, so any field-name mismatch, routing
|
||||
* bug, or wiring issue between controller and validator is caught.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CertificateValidationIntegrationTest {
|
||||
|
||||
// Infrastructure mocks — not under test
|
||||
@Mock private WorkflowSessionService workflowSessionService;
|
||||
@Mock private WorkflowParticipantRepository participantRepository;
|
||||
@Mock private MetadataEncryptionService metadataEncryptionService;
|
||||
|
||||
// Mock PdfSigningService so the test-sign step succeeds without a real PDF engine
|
||||
@Mock private PdfSigningService pdfSigningService;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
private static final String TOKEN = "integration-test-token";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
// Use the REAL validator wired with the mock signing service
|
||||
CertificateSubmissionValidator realValidator =
|
||||
new CertificateSubmissionValidator(pdfSigningService);
|
||||
|
||||
WorkflowParticipantController controller =
|
||||
new WorkflowParticipantController(
|
||||
workflowSessionService,
|
||||
participantRepository,
|
||||
new ObjectMapper(),
|
||||
metadataEncryptionService,
|
||||
realValidator);
|
||||
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
// Return a non-expired participant for all tests
|
||||
WorkflowParticipant participant = new WorkflowParticipant();
|
||||
when(participantRepository.findByShareToken(TOKEN)).thenReturn(Optional.of(participant));
|
||||
|
||||
// test-sign succeeds — only reached by valid-cert tests, lenient to avoid
|
||||
// UnnecessaryStubbingException on error-path tests (wrong password, expired, etc.)
|
||||
org.mockito.Mockito.lenient()
|
||||
.when(
|
||||
pdfSigningService.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
isNull(),
|
||||
anyString(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
anyBoolean()))
|
||||
.thenReturn(new byte[0]);
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private static byte[] loadCert(String filename) throws Exception {
|
||||
try (InputStream in =
|
||||
CertificateValidationIntegrationTest.class.getResourceAsStream(
|
||||
"/test-certs/" + filename)) {
|
||||
if (in == null) throw new IllegalStateException("cert not found: " + filename);
|
||||
return in.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private static MockMultipartFile p12Part(String filename) throws Exception {
|
||||
return new MockMultipartFile(
|
||||
"p12File", filename, "application/octet-stream", loadCert(filename));
|
||||
}
|
||||
|
||||
private static MockMultipartFile jksPart(String filename) throws Exception {
|
||||
return new MockMultipartFile(
|
||||
"jksFile", filename, "application/octet-stream", loadCert(filename));
|
||||
}
|
||||
|
||||
// ---- tests ----
|
||||
|
||||
@Test
|
||||
void validP12_returnsValidTrueWithSubjectName() throws Exception {
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(p12Part("valid-test.p12"))
|
||||
.param("participantToken", TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "testpass"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(true))
|
||||
.andExpect(jsonPath("$.subjectName").isNotEmpty())
|
||||
.andExpect(jsonPath("$.notAfter").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrongPassword_returnsValidFalseWithErrorMessage() throws Exception {
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(p12Part("valid-test.p12"))
|
||||
.param("participantToken", TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "wrongpassword"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(false))
|
||||
.andExpect(
|
||||
jsonPath("$.error")
|
||||
.value("Invalid certificate password or corrupt keystore file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredP12_returnsValidFalseWithExpiryMessage() throws Exception {
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(p12Part("expired-test.p12"))
|
||||
.param("participantToken", TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "testpass"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(false))
|
||||
.andExpect(
|
||||
jsonPath("$.error").value(org.hamcrest.Matchers.containsString("expired")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void notYetValidP12_returnsValidFalseWithNotYetValidMessage() throws Exception {
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(p12Part("not-yet-valid-test.p12"))
|
||||
.param("participantToken", TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "testpass"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(false))
|
||||
.andExpect(
|
||||
jsonPath("$.error")
|
||||
.value(org.hamcrest.Matchers.containsString("not yet valid")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validJks_returnsValidTrueWithSubjectName() throws Exception {
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(jksPart("valid-test.jks"))
|
||||
.param("participantToken", TOKEN)
|
||||
.param("certType", "JKS")
|
||||
.param("password", "jkspass"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(true))
|
||||
.andExpect(jsonPath("$.subjectName").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverCertType_returnsValidTrueWithoutFileUpload() throws Exception {
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.param("participantToken", TOKEN)
|
||||
.param("certType", "SERVER"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(true));
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package stirling.software.proprietary.workflow.controller;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import stirling.software.proprietary.workflow.dto.CertificateInfo;
|
||||
import stirling.software.proprietary.workflow.model.WorkflowParticipant;
|
||||
import stirling.software.proprietary.workflow.repository.WorkflowParticipantRepository;
|
||||
import stirling.software.proprietary.workflow.service.CertificateSubmissionValidator;
|
||||
import stirling.software.proprietary.workflow.service.MetadataEncryptionService;
|
||||
import stirling.software.proprietary.workflow.service.WorkflowSessionService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WorkflowParticipantValidateCertificateTest {
|
||||
|
||||
@Mock private WorkflowSessionService workflowSessionService;
|
||||
@Mock private WorkflowParticipantRepository participantRepository;
|
||||
@Mock private MetadataEncryptionService metadataEncryptionService;
|
||||
@Mock private CertificateSubmissionValidator certificateSubmissionValidator;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
private static final String VALID_TOKEN = "valid-share-token-abc123";
|
||||
private static final byte[] DUMMY_CERT = "dummy-cert-bytes".getBytes();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
WorkflowParticipantController controller =
|
||||
new WorkflowParticipantController(
|
||||
workflowSessionService,
|
||||
participantRepository,
|
||||
new ObjectMapper(),
|
||||
metadataEncryptionService,
|
||||
certificateSubmissionValidator);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
}
|
||||
|
||||
private WorkflowParticipant activeParticipant() {
|
||||
WorkflowParticipant p = new WorkflowParticipant();
|
||||
// isExpired() returns false by default (no expiry date set)
|
||||
return p;
|
||||
}
|
||||
|
||||
// ---- Happy path: valid cert ----
|
||||
|
||||
@Test
|
||||
void validCertificate_returns200WithValidTrue() throws Exception {
|
||||
when(participantRepository.findByShareToken(VALID_TOKEN))
|
||||
.thenReturn(Optional.of(activeParticipant()));
|
||||
|
||||
CertificateInfo info =
|
||||
new CertificateInfo(
|
||||
"Test Signer",
|
||||
"Test CA",
|
||||
new Date(),
|
||||
new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000),
|
||||
true);
|
||||
when(certificateSubmissionValidator.validateAndExtractInfo(any(), eq("P12"), eq("secret")))
|
||||
.thenReturn(info);
|
||||
|
||||
MockMultipartFile certFile =
|
||||
new MockMultipartFile(
|
||||
"p12File", "cert.p12", "application/octet-stream", DUMMY_CERT);
|
||||
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(certFile)
|
||||
.param("participantToken", VALID_TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "secret"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(true))
|
||||
.andExpect(jsonPath("$.subjectName").value("Test Signer"));
|
||||
}
|
||||
|
||||
// ---- Bad password / invalid cert → validator throws 400, we return 200 valid:false ----
|
||||
|
||||
@Test
|
||||
void invalidCertificate_returns200WithValidFalseAndErrorMessage() throws Exception {
|
||||
when(participantRepository.findByShareToken(VALID_TOKEN))
|
||||
.thenReturn(Optional.of(activeParticipant()));
|
||||
|
||||
when(certificateSubmissionValidator.validateAndExtractInfo(any(), any(), any()))
|
||||
.thenThrow(
|
||||
new ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Invalid certificate password or corrupt keystore file"));
|
||||
|
||||
MockMultipartFile certFile =
|
||||
new MockMultipartFile(
|
||||
"p12File", "cert.p12", "application/octet-stream", DUMMY_CERT);
|
||||
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(certFile)
|
||||
.param("participantToken", VALID_TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "wrong"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.valid").value(false))
|
||||
.andExpect(
|
||||
jsonPath("$.error")
|
||||
.value("Invalid certificate password or corrupt keystore file"));
|
||||
}
|
||||
|
||||
// ---- No file → 400 bad request ----
|
||||
|
||||
@Test
|
||||
void missingCertFile_returns400() throws Exception {
|
||||
when(participantRepository.findByShareToken(VALID_TOKEN))
|
||||
.thenReturn(Optional.of(activeParticipant()));
|
||||
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.param("participantToken", VALID_TOKEN)
|
||||
.param("certType", "P12")
|
||||
.param("password", "pass"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ---- Invalid / expired token → 403 ----
|
||||
|
||||
@Test
|
||||
void invalidToken_returns403() throws Exception {
|
||||
when(participantRepository.findByShareToken("bad-token")).thenReturn(Optional.empty());
|
||||
|
||||
MockMultipartFile certFile =
|
||||
new MockMultipartFile(
|
||||
"p12File", "cert.p12", "application/octet-stream", DUMMY_CERT);
|
||||
|
||||
mockMvc.perform(
|
||||
multipart("/api/v1/workflow/participant/validate-certificate")
|
||||
.file(certFile)
|
||||
.param("participantToken", "bad-token")
|
||||
.param("certType", "P12")
|
||||
.param("password", "pass"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
package stirling.software.proprietary.workflow.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import stirling.software.common.service.PdfSigningService;
|
||||
import stirling.software.proprietary.workflow.dto.CertificateInfo;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CertificateSubmissionValidatorTest {
|
||||
|
||||
@Mock private PdfSigningService pdfSigningService;
|
||||
|
||||
private CertificateSubmissionValidator validator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
validator = new CertificateSubmissionValidator(pdfSigningService);
|
||||
}
|
||||
|
||||
// ---- helper: build a PKCS12 keystore with a self-signed cert ----
|
||||
|
||||
private static byte[] buildP12Keystore(
|
||||
String alias, String password, Date notBefore, Date notAfter) throws Exception {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
KeyPair kp = kpg.generateKeyPair();
|
||||
|
||||
X500Name subject = new X500Name("CN=Test Signer,O=Test Org,C=GB");
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
|
||||
X509Certificate cert =
|
||||
new JcaX509CertificateConverter()
|
||||
.getCertificate(
|
||||
new JcaX509v3CertificateBuilder(
|
||||
subject,
|
||||
BigInteger.valueOf(System.currentTimeMillis()),
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
kp.getPublic())
|
||||
.build(signer));
|
||||
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(null, null);
|
||||
ks.setKeyEntry(alias, kp.getPrivate(), password.toCharArray(), new Certificate[] {cert});
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ks.store(bos, password.toCharArray());
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
private static byte[] validP12(String password) throws Exception {
|
||||
Date now = new Date();
|
||||
Date future = new Date(now.getTime() + 365L * 24 * 60 * 60 * 1000);
|
||||
return buildP12Keystore("test", password, now, future);
|
||||
}
|
||||
|
||||
private static byte[] expiredP12(String password) throws Exception {
|
||||
Date past1 = new Date(System.currentTimeMillis() - 10_000_000L);
|
||||
Date past2 = new Date(System.currentTimeMillis() - 1_000L);
|
||||
return buildP12Keystore("test", password, past1, past2);
|
||||
}
|
||||
|
||||
private static byte[] notYetValidP12(String password) throws Exception {
|
||||
Date future1 = new Date(System.currentTimeMillis() + 10_000_000L);
|
||||
Date future2 = new Date(System.currentTimeMillis() + 20_000_000L);
|
||||
return buildP12Keystore("test", password, future1, future2);
|
||||
}
|
||||
|
||||
// ---- SERVER type: skip validation ----
|
||||
|
||||
@Test
|
||||
void serverCertType_returnsNull_withoutCallingSigningService() throws Exception {
|
||||
CertificateInfo result = validator.validateAndExtractInfo(new byte[0], "SERVER", "pass");
|
||||
|
||||
assertThat(result).isNull();
|
||||
verify(pdfSigningService, never())
|
||||
.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullCertType_returnsNull_withoutCallingSigningService() throws Exception {
|
||||
CertificateInfo result = validator.validateAndExtractInfo(new byte[0], null, "pass");
|
||||
|
||||
assertThat(result).isNull();
|
||||
verify(pdfSigningService, never())
|
||||
.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean());
|
||||
}
|
||||
|
||||
// ---- Valid P12 certificate ----
|
||||
|
||||
@Test
|
||||
void validP12Certificate_returnsInfo() throws Exception {
|
||||
byte[] p12 = validP12("password");
|
||||
when(pdfSigningService.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
isNull(),
|
||||
anyString(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
anyBoolean()))
|
||||
.thenReturn(new byte[0]);
|
||||
|
||||
CertificateInfo info = validator.validateAndExtractInfo(p12, "P12", "password");
|
||||
|
||||
assertThat(info).isNotNull();
|
||||
assertThat(info.subjectName()).isEqualTo("Test Signer");
|
||||
assertThat(info.notAfter()).isNotNull();
|
||||
assertThat(info.notAfter()).isAfter(new Date());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validPkcs12Alias_acceptedAsCertType() throws Exception {
|
||||
byte[] p12 = validP12("password");
|
||||
when(pdfSigningService.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
isNull(),
|
||||
anyString(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
anyBoolean()))
|
||||
.thenReturn(new byte[0]);
|
||||
|
||||
CertificateInfo info = validator.validateAndExtractInfo(p12, "PKCS12", "password");
|
||||
|
||||
assertThat(info).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validPfxAlias_acceptedAsCertType() throws Exception {
|
||||
byte[] p12 = validP12("password");
|
||||
when(pdfSigningService.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
isNull(),
|
||||
anyString(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
anyBoolean()))
|
||||
.thenReturn(new byte[0]);
|
||||
|
||||
CertificateInfo info = validator.validateAndExtractInfo(p12, "PFX", "password");
|
||||
|
||||
assertThat(info).isNotNull();
|
||||
}
|
||||
|
||||
// ---- Wrong password ----
|
||||
|
||||
@Test
|
||||
void wrongPassword_throws400() {
|
||||
byte[] p12;
|
||||
try {
|
||||
p12 = validP12("correct-password");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
assertThatThrownBy(() -> validator.validateAndExtractInfo(p12, "P12", "wrong-password"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(
|
||||
ex ->
|
||||
assertThat(((ResponseStatusException) ex).getStatusCode())
|
||||
.isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
// ---- Corrupt keystore bytes ----
|
||||
|
||||
@Test
|
||||
void corruptBytes_throws400() {
|
||||
byte[] garbage = "this is not a keystore".getBytes();
|
||||
|
||||
assertThatThrownBy(() -> validator.validateAndExtractInfo(garbage, "P12", "password"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(
|
||||
ex ->
|
||||
assertThat(((ResponseStatusException) ex).getStatusCode())
|
||||
.isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
// ---- Expired certificate ----
|
||||
|
||||
@Test
|
||||
void expiredCertificate_throws400WithExpiryDateInMessage() {
|
||||
byte[] p12;
|
||||
try {
|
||||
p12 = expiredP12("password");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
assertThatThrownBy(() -> validator.validateAndExtractInfo(p12, "P12", "password"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(
|
||||
ex -> {
|
||||
ResponseStatusException rse = (ResponseStatusException) ex;
|
||||
assertThat(rse.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(rse.getReason()).contains("expired");
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Not-yet-valid certificate ----
|
||||
|
||||
@Test
|
||||
void notYetValidCertificate_throws400() {
|
||||
byte[] p12;
|
||||
try {
|
||||
p12 = notYetValidP12("password");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
assertThatThrownBy(() -> validator.validateAndExtractInfo(p12, "P12", "password"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(
|
||||
ex -> {
|
||||
ResponseStatusException rse = (ResponseStatusException) ex;
|
||||
assertThat(rse.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(rse.getReason()).contains("not yet valid");
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Test-sign failure ----
|
||||
|
||||
@Test
|
||||
void signingServiceThrows_wrapsAs400() throws Exception {
|
||||
byte[] p12 = validP12("password");
|
||||
doThrow(new RuntimeException("algorithm not supported"))
|
||||
.when(pdfSigningService)
|
||||
.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
isNull(),
|
||||
anyString(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
anyBoolean());
|
||||
|
||||
assertThatThrownBy(() -> validator.validateAndExtractInfo(p12, "P12", "password"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(
|
||||
ex -> {
|
||||
ResponseStatusException rse = (ResponseStatusException) ex;
|
||||
assertThat(rse.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(rse.getReason()).contains("compatible");
|
||||
});
|
||||
}
|
||||
|
||||
// ---- JKS keystore ----
|
||||
|
||||
@Test
|
||||
void validJksKeystore_returnsInfo() throws Exception {
|
||||
// Build a JKS keystore with the same cert
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(2048);
|
||||
KeyPair kp = kpg.generateKeyPair();
|
||||
|
||||
X500Name subject = new X500Name("CN=JKS Signer,O=Test,C=GB");
|
||||
Date now = new Date();
|
||||
Date future = new Date(now.getTime() + 365L * 24 * 60 * 60 * 1000);
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
|
||||
X509Certificate cert =
|
||||
new JcaX509CertificateConverter()
|
||||
.getCertificate(
|
||||
new JcaX509v3CertificateBuilder(
|
||||
subject,
|
||||
BigInteger.valueOf(System.currentTimeMillis()),
|
||||
now,
|
||||
future,
|
||||
subject,
|
||||
kp.getPublic())
|
||||
.build(signer));
|
||||
|
||||
KeyStore jks = KeyStore.getInstance("JKS");
|
||||
jks.load(null, null);
|
||||
jks.setKeyEntry(
|
||||
"jks-test", kp.getPrivate(), "jkspass".toCharArray(), new Certificate[] {cert});
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
jks.store(bos, "jkspass".toCharArray());
|
||||
byte[] jksBytes = bos.toByteArray();
|
||||
|
||||
when(pdfSigningService.signWithKeystore(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
anyBoolean(),
|
||||
isNull(),
|
||||
anyString(),
|
||||
isNull(),
|
||||
isNull(),
|
||||
anyBoolean()))
|
||||
.thenReturn(new byte[0]);
|
||||
|
||||
CertificateInfo info = validator.validateAndExtractInfo(jksBytes, "JKS", "jkspass");
|
||||
|
||||
assertThat(info).isNotNull();
|
||||
assertThat(info.subjectName()).isEqualTo("JKS Signer");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -177,8 +177,6 @@ subprojects {
|
||||
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
|
||||
// Force a compatible Guava version for spotless
|
||||
resolutionStrategy.force 'com.google.guava:guava:33.0.0-jre'
|
||||
|
||||
// Security CVE fixes - hardcoded resolution strategy to ensure safe versions
|
||||
// Primary fixes via explicit dependencies in app/core/build.gradle:
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Fat-Disable-Endpoints
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: docker/embedded/Dockerfile.fat
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:fat
|
||||
build:
|
||||
context: ../../../
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security-Fat
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: docker/embedded/Dockerfile.fat
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:fat
|
||||
build:
|
||||
context: ../../../
|
||||
dockerfile: docker/embedded/Dockerfile
|
||||
dockerfile: docker/embedded/Dockerfile.fat
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: docker/embedded/Dockerfile.ultra-lite
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:ultra-lite
|
||||
build:
|
||||
context: ../../../
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security-Fat-with-login
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: docker/embedded/Dockerfile.fat
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:fat
|
||||
build:
|
||||
context: ../../../
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Stirling AI Engine Guide
|
||||
|
||||
This file is for AI agents working in `engine/`.
|
||||
|
||||
The engine is a Python reasoning service for Stirling. It plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Keep `make check` passing.
|
||||
- Use modern Python when it improves clarity.
|
||||
- Prefer explicit names to cleverness.
|
||||
- Avoid nested functions and nested classes unless the language construct requires them.
|
||||
- Prefer composition to inheritance when combining concepts.
|
||||
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
|
||||
- Add comments sparingly and only when they explain non-obvious intent.
|
||||
|
||||
### Typing and Models
|
||||
|
||||
- Deserialize into Pydantic models as early as possible.
|
||||
- Serialize from Pydantic models as late as possible.
|
||||
- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.
|
||||
- Avoid `Any` wherever possible.
|
||||
- Avoid `cast()` wherever possible (reconsider the structure first).
|
||||
- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.
|
||||
- Do not use string literals for any type annotations, including `cast()`.
|
||||
|
||||
### Configuration
|
||||
|
||||
- Keep application-owned configuration in `stirling.config`.
|
||||
- Only add `STIRLING_*` environment variables that the engine itself truly owns.
|
||||
- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.
|
||||
- Let `pydantic-ai` own provider authentication configuration when possible.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Package Roles
|
||||
|
||||
- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.
|
||||
- `stirling.models`: shared model primitives and generated tool models.
|
||||
- `stirling.agents`: reasoning modules for individual capabilities.
|
||||
- `stirling.api`: HTTP layer, dependency access, and app startup wiring.
|
||||
- `stirling.services`: shared runtime and non-AI infrastructure.
|
||||
- `stirling.config`: application-owned settings.
|
||||
|
||||
### Source Of Truth
|
||||
|
||||
- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.
|
||||
- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.
|
||||
- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.
|
||||
- If a tool ID must match a parameter model, validate that relationship explicitly in code.
|
||||
|
||||
### Boundaries
|
||||
|
||||
- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.
|
||||
- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.
|
||||
- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.
|
||||
- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.
|
||||
|
||||
## AI Usage
|
||||
|
||||
- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.
|
||||
- Use AI for reasoning-heavy outputs, not deterministic glue.
|
||||
- Do not ask the model to invent data that Python can derive safely.
|
||||
- Do not fabricate fallback user-facing copy in code to hide incomplete model output.
|
||||
- AI output schemas should be impossible to instantiate incorrectly.
|
||||
- Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.
|
||||
- Prefer Python to derive deterministic follow-up structure from a valid AI result.
|
||||
- Use `NativeOutput(...)` for structured model outputs.
|
||||
- Use `ToolOutput(...)` when the model should select and call delegate functions.
|
||||
|
||||
## Testing
|
||||
|
||||
- Test contracts directly.
|
||||
- Test agents directly where behaviour matters.
|
||||
- Test API routes as thin integration points.
|
||||
- Prefer dependency overrides or startup-state seams to monkeypatching random globals.
|
||||
+4
-50
@@ -1,63 +1,17 @@
|
||||
# syntax=docker/dockerfile:1.5
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
|
||||
# Install system deps: Node.js + Puppeteer/Chromium runtime libraries
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
poppler-utils \
|
||||
nodejs \
|
||||
npm \
|
||||
# Fonts for correct text rendering in generated PDFs
|
||||
fonts-liberation \
|
||||
fonts-dejavu-core \
|
||||
# Chromium headless runtime deps
|
||||
libnss3 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libpango-1.0-0 \
|
||||
libcairo2 \
|
||||
libasound2 \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
libgbm1 \
|
||||
libdrm2 \
|
||||
libxshmfence1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Node dependencies (Puppeteer) first for layer caching
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Install Python dependencies
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
# Copy Python source into /app/src/
|
||||
COPY src/ ./src/
|
||||
|
||||
# Create output directories
|
||||
RUN mkdir -p /app/output /app/data
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5001
|
||||
|
||||
# Environment variables for Gunicorn configuration
|
||||
ENV GUNICORN_WORKERS=4
|
||||
ENV GUNICORN_TIMEOUT=120
|
||||
# Disable Python stdout/stderr buffering so log output appears immediately in Docker logs
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
WORKDIR /app/src
|
||||
CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:5001 --workers ${GUNICORN_WORKERS} --timeout ${GUNICORN_TIMEOUT} --worker-class gthread --threads 4 app:app"]
|
||||
EXPOSE 5001
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001"]
|
||||
|
||||
+4
-48
@@ -1,59 +1,15 @@
|
||||
# syntax=docker/dockerfile:1.5
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
|
||||
# Install system deps: Node.js + Puppeteer/Chromium runtime libraries
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
poppler-utils \
|
||||
nodejs \
|
||||
npm \
|
||||
# Fonts for correct text rendering in generated PDFs
|
||||
fonts-liberation \
|
||||
fonts-dejavu-core \
|
||||
# Chromium headless runtime deps
|
||||
libnss3 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxrandr2 \
|
||||
libpango-1.0-0 \
|
||||
libcairo2 \
|
||||
libasound2 \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Node dependencies (Puppeteer) first for layer caching
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy only pyproject.toml and uv.lock first for dependency caching
|
||||
COPY pyproject.toml uv.lock ./
|
||||
|
||||
# Install dependencies including dev extras
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen
|
||||
|
||||
# Create output directories
|
||||
RUN mkdir -p /app/output /app/data
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5001
|
||||
|
||||
# Set environment for Flask development
|
||||
ENV FLASK_ENV=development
|
||||
ENV FLASK_DEBUG=1
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Run the Flask app with hot reload
|
||||
# Note: src/ will be mounted as a volume at runtime
|
||||
WORKDIR /app/src
|
||||
CMD ["uv", "run", "python", "app.py"]
|
||||
EXPOSE 5001
|
||||
|
||||
CMD ["uv", "run", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001", "--reload"]
|
||||
|
||||
+7
-12
@@ -4,8 +4,7 @@ REPO_ROOT_DIR = ..
|
||||
ROOT_DIR = .
|
||||
VENV_DIR = $(ROOT_DIR)/.venv
|
||||
DEPS_STAMP := $(VENV_DIR)/.deps-installed
|
||||
NODE_DEPS_STAMP := node_modules/.deps-installed
|
||||
TOOL_MODELS := $(CURDIR)/src/models/tool_models.py
|
||||
TOOL_MODELS := $(CURDIR)/src/stirling/models/tool_models.py
|
||||
FRONTEND_DIR := $(REPO_ROOT_DIR)/frontend
|
||||
FRONTEND_TSX := $(FRONTEND_DIR)/node_modules/.bin/tsx
|
||||
|
||||
@@ -14,10 +13,6 @@ $(DEPS_STAMP): $(ROOT_DIR)/uv.lock $(ROOT_DIR)/pyproject.toml
|
||||
uv sync
|
||||
touch $(DEPS_STAMP)
|
||||
|
||||
$(NODE_DEPS_STAMP): package-lock.json
|
||||
npm ci
|
||||
touch $(NODE_DEPS_STAMP)
|
||||
|
||||
help:
|
||||
@echo "Engine commands:"
|
||||
@echo " make install - Install production dependencies"
|
||||
@@ -27,14 +22,14 @@ help:
|
||||
@echo " make format-check - Show whether code is correctly formatted"
|
||||
@echo " make typecheck - Run type checking"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make run - Run the Flask backend with gunicorn (production)"
|
||||
@echo " make run-dev - Run the Flask backend with dev server (development only)"
|
||||
@echo " make tool-models - Generate src/models/tool_models_*.py from frontend TypeScript tool defs"
|
||||
@echo " make run - Run the FastAPI backend with uvicorn"
|
||||
@echo " make run-dev - Run the FastAPI backend with reload"
|
||||
@echo " make tool-models - Generate src/stirling/models/tool_models.py from frontend TypeScript tool defs"
|
||||
@echo " make clean - Clean up generated files"
|
||||
@echo " make docker-build - Build Docker image"
|
||||
@echo " make docker-run - Run Docker container"
|
||||
|
||||
install: $(DEPS_STAMP) $(NODE_DEPS_STAMP)
|
||||
install: $(DEPS_STAMP)
|
||||
|
||||
prep: install
|
||||
uv run scripts/setup_env.py
|
||||
@@ -65,10 +60,10 @@ test: prep
|
||||
uv run pytest tests
|
||||
|
||||
run: prep
|
||||
cd src && PYTHONUNBUFFERED=1 uv run gunicorn --bind 0.0.0.0:5001 --workers 4 --timeout 120 --worker-class gthread --threads 4 app:app
|
||||
cd src && PYTHONUNBUFFERED=1 uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001
|
||||
|
||||
run-dev: prep
|
||||
cd src && PYTHONUNBUFFERED=1 uv run python app.py
|
||||
cd src && PYTHONUNBUFFERED=1 uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001 --reload
|
||||
|
||||
frontend-deps:
|
||||
if [ ! -x "$(FRONTEND_TSX)" ]; then cd $(FRONTEND_DIR) && npm install; fi
|
||||
|
||||
@@ -1,51 +1,9 @@
|
||||
# Claude API (Anthropic)
|
||||
STIRLING_ANTHROPIC_API_KEY=
|
||||
# Configure the model strings passed to pydantic-ai. Provider credentials are handled by
|
||||
# pydantic-ai and should be set using the provider's native environment variables, for example
|
||||
# ANTHROPIC_API_KEY or OPENAI_API_KEY.
|
||||
STIRLING_SMART_MODEL=anthropic:claude-haiku-4-5
|
||||
STIRLING_FAST_MODEL=anthropic:claude-haiku-4-5
|
||||
|
||||
# OpenAI API (Optional - for GPT models)
|
||||
STIRLING_OPENAI_API_KEY=
|
||||
STIRLING_OPENAI_BASE_URL=
|
||||
|
||||
# Model Configuration
|
||||
STIRLING_SMART_MODEL=claude-sonnet-4-5-20250929
|
||||
STIRLING_FAST_MODEL=claude-haiku-4-5-20251001
|
||||
|
||||
# GPT-5 Reasoning Settings (only used if SMART_MODEL is gpt-5)
|
||||
# Options: minimal, low, medium, high, xhigh
|
||||
STIRLING_SMART_MODEL_REASONING_EFFORT=medium
|
||||
STIRLING_FAST_MODEL_REASONING_EFFORT=minimal
|
||||
|
||||
# GPT-5 Text Verbosity (only used if SMART_MODEL is gpt-5)
|
||||
# Options: minimal, low, medium, high
|
||||
STIRLING_SMART_MODEL_TEXT_VERBOSITY=medium
|
||||
STIRLING_FAST_MODEL_TEXT_VERBOSITY=low
|
||||
|
||||
# Output token limits. STIRLING_AI_MAX_TOKENS is a global override (leave empty to use per-model defaults)
|
||||
STIRLING_AI_MAX_TOKENS=
|
||||
# Default output token limits applied by the engine for each model tier.
|
||||
STIRLING_SMART_MODEL_MAX_TOKENS=8192
|
||||
STIRLING_FAST_MODEL_MAX_TOKENS=2048
|
||||
STIRLING_CLAUDE_MAX_TOKENS=4096
|
||||
STIRLING_DEFAULT_MODEL_MAX_TOKENS=4096
|
||||
|
||||
# PostHog Analytics
|
||||
STIRLING_POSTHOG_API_KEY=
|
||||
STIRLING_POSTHOG_HOST=https://eu.i.posthog.com
|
||||
|
||||
# Backend Configuration
|
||||
STIRLING_JAVA_BACKEND_URL=http://localhost:8080
|
||||
STIRLING_JAVA_BACKEND_API_KEY=123456789
|
||||
STIRLING_JAVA_REQUEST_TIMEOUT_SECONDS=30
|
||||
|
||||
# Logging and Debugging
|
||||
STIRLING_AI_RAW_DEBUG=0
|
||||
STIRLING_FLASK_DEBUG=0
|
||||
# Override the log file directory (defaults to ./logs in server mode, OS log dir in desktop mode)
|
||||
STIRLING_LOG_PATH=
|
||||
# Enable verbose table detection debug logging in the PDF text editor
|
||||
STIRLING_PDF_EDITOR_TABLE_DEBUG=0
|
||||
# Set to true when running as a Tauri desktop app (switches to OS-native log directory)
|
||||
STIRLING_PDF_TAURI_MODE=false
|
||||
|
||||
# AI Settings
|
||||
STIRLING_AI_STREAMING=true
|
||||
STIRLING_AI_PREVIEW_MAX_INFLIGHT=3
|
||||
STIRLING_AI_REQUEST_TIMEOUT=70
|
||||
|
||||
Generated
-1138
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "stirling-docgen",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "Node tooling for Stirling PDF docgen backend (HTML→PDF via Puppeteer)",
|
||||
"scripts": {
|
||||
"postinstall": "node -e \"require('puppeteer')\" 2>/dev/null || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"puppeteer": "^24.25.0"
|
||||
}
|
||||
}
|
||||
+7
-15
@@ -4,28 +4,19 @@ version = "0.1.0"
|
||||
description = "AI Document Engine"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"flask>=3.0.0",
|
||||
"flask-cors>=4.0.0",
|
||||
"gunicorn>=25.0.3",
|
||||
"openai>=2.20.0",
|
||||
"langchain-core>=1.2.11",
|
||||
"langchain-openai>=1.1.9",
|
||||
"langchain-anthropic>=1.1.9",
|
||||
"platformdirs>=4.5.1",
|
||||
"pypdf>=6.7.0",
|
||||
"fastapi>=0.116.0",
|
||||
"pydantic>=2.0.0",
|
||||
"posthog>=7.8.6",
|
||||
"pydantic-ai>=1.67.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"beautifulsoup4>=4.12.0",
|
||||
"uvicorn>=0.35.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"ruff>=0.14.10",
|
||||
"pyright>=1.1.408",
|
||||
"datamodel-code-generator[ruff]>=0.27.0",
|
||||
"ruff>=0.14.10",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
@@ -61,8 +52,9 @@ pythonVersion = "3.13"
|
||||
|
||||
reportImportCycles = "warning"
|
||||
reportUnnecessaryCast = "warning"
|
||||
reportUnnecessaryTypeIgnoreComment = "warning"
|
||||
reportUnusedImport = "warning"
|
||||
#reportUnknownParameterType = "warning"
|
||||
reportUnknownParameterType = "warning"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
@@ -378,7 +378,7 @@ def write_models_module(out_path: Path, specs: list[ToolModelSpec]) -> None:
|
||||
"from __future__ import annotations\n\n",
|
||||
"from enum import StrEnum\n",
|
||||
"from typing import Any, Literal\n\n",
|
||||
"from models.base import ApiModel\n",
|
||||
"from stirling.models.base import ApiModel\n",
|
||||
]
|
||||
|
||||
class_names: dict[str, str] = {spec.tool_id: _to_class_name(spec.tool_id) for spec in specs}
|
||||
@@ -500,7 +500,7 @@ def main() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
specs = discover_tool_specs(repo_root)
|
||||
|
||||
output_path = Path(args.output) if args.output else (repo_root / "docgen/backend/models/tool_models.py")
|
||||
output_path = Path(args.output) if args.output else (repo_root / "src/stirling/models/tool_models.py")
|
||||
|
||||
write_models_module(output_path, specs)
|
||||
print(f"Wrote {len(specs)} tool model specs")
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import models
|
||||
from config import SMART_MODEL, STREAMING_ENABLED
|
||||
from format_prompts import get_format_prompt
|
||||
from html_utils import inject_theme
|
||||
from llm_utils import run_ai, stream_ai
|
||||
from prompts import (
|
||||
field_values_system_prompt,
|
||||
html_context_messages,
|
||||
html_edit_system_prompt,
|
||||
html_system_prompt,
|
||||
outline_generator_system_prompt,
|
||||
section_draft_system_prompt,
|
||||
template_fill_html_system_prompt,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_outline_with_llm(
|
||||
prompt: str,
|
||||
document_type: str,
|
||||
constraints: models.Constraint | None = None,
|
||||
) -> models.OutlineResponse:
|
||||
"""
|
||||
Generate outline for a specific document type.
|
||||
|
||||
document_type should already be detected - this function only generates the outline.
|
||||
"""
|
||||
constraint_text = ""
|
||||
if constraints:
|
||||
tone = constraints.tone
|
||||
audience = constraints.audience
|
||||
pages = constraints.page_count
|
||||
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
|
||||
|
||||
# Try to get a format-specific extraction prompt
|
||||
format_prompt, default_sections = get_format_prompt(document_type)
|
||||
system_prompt = outline_generator_system_prompt(document_type, constraint_text, format_prompt, default_sections)
|
||||
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=prompt),
|
||||
]
|
||||
|
||||
parsed = run_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
models.OutlineResponse,
|
||||
tag="outline",
|
||||
)
|
||||
|
||||
if parsed:
|
||||
logger.info(
|
||||
"[OUTLINE] Generated outline doc_type=%s sections=%d filename=%s",
|
||||
parsed.doc_type,
|
||||
len(parsed.sections),
|
||||
parsed.outline_filename,
|
||||
)
|
||||
return parsed
|
||||
|
||||
raise RuntimeError("AI outline generation failed.")
|
||||
|
||||
|
||||
def generate_field_values(
|
||||
prompt: str,
|
||||
document_type: str,
|
||||
fields: list[dict[str, Any]],
|
||||
constraints: models.Constraint | None = None,
|
||||
) -> list[dict[str, str]]:
|
||||
constraint_text = ""
|
||||
if constraints:
|
||||
tone = constraints.tone
|
||||
audience = constraints.audience
|
||||
pages = constraints.page_count
|
||||
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
|
||||
system_prompt = field_values_system_prompt(constraint_text)
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=f"Document type: {document_type}"),
|
||||
models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"),
|
||||
models.ChatMessage(role="user", content=f"Fields:\n{json.dumps(fields, ensure_ascii=True)}"),
|
||||
]
|
||||
parsed = run_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
models.LLMFieldValuesResponse,
|
||||
tag="field_values",
|
||||
)
|
||||
if parsed:
|
||||
return [{"label": item.label, "value": item.value} for item in parsed.fields]
|
||||
|
||||
raise RuntimeError("AI field extraction failed.")
|
||||
|
||||
|
||||
def generate_section_draft(
|
||||
prompt: str,
|
||||
document_type: str,
|
||||
outline_text: str,
|
||||
constraints: models.Constraint | None = None,
|
||||
) -> list[models.DraftSection]:
|
||||
constraint_text = ""
|
||||
if constraints:
|
||||
tone = constraints.tone
|
||||
audience = constraints.audience
|
||||
pages = constraints.page_count
|
||||
constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
|
||||
system_prompt = section_draft_system_prompt(constraint_text)
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=f"Document type: {document_type}"),
|
||||
models.ChatMessage(role="user", content=f"Outline:\n{outline_text}"),
|
||||
models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"),
|
||||
]
|
||||
parsed = run_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
models.LLMDraftSectionsResponse,
|
||||
tag="section_draft",
|
||||
)
|
||||
return parsed.sections
|
||||
|
||||
|
||||
# ── HTML generation ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_template_fill_html_stream(
|
||||
template_html: str,
|
||||
document_type: str,
|
||||
outline_text: str,
|
||||
draft_sections: list[models.DraftSection] | None = None,
|
||||
constraints: models.Constraint | None = None,
|
||||
theme: dict[str, str] | None = None,
|
||||
additional_instructions: str | None = None,
|
||||
has_logo: bool = False,
|
||||
):
|
||||
"""Fill an HTML template by replacing {{PLACEHOLDER}} tokens, streaming chunks."""
|
||||
constraints_text = ""
|
||||
if constraints:
|
||||
tone = constraints.tone
|
||||
audience = constraints.audience
|
||||
pages = constraints.page_count
|
||||
constraints_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}."
|
||||
if draft_sections:
|
||||
constraints_text += "\nUse the section content to inform placeholder values."
|
||||
|
||||
# Inject theme before sending to AI if overrides are provided
|
||||
if theme:
|
||||
template_html = inject_theme(template_html, theme)
|
||||
|
||||
system_prompt = template_fill_html_system_prompt(constraints_text)
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=f"Document type: {document_type}"),
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=(
|
||||
"CRITICAL EXAMPLE — What to preserve vs. what to change:\n"
|
||||
"If template has:\n"
|
||||
" <div class='vendor-name'>{{VENDOR_NAME}}</div>\n"
|
||||
"You MUST output:\n"
|
||||
" <div class='vendor-name'>Acme Corporation</div> ← fill the token, keep the tag\n"
|
||||
"\n"
|
||||
"For multi-row content like {{LINEITEM_ROWS}}, generate full <tr>...</tr> HTML.\n"
|
||||
"Match ONLY the columns present in the template's <thead> — do NOT add extra columns.\n"
|
||||
"Example (4-column table: #, Description, Qty, Line Total):\n"
|
||||
" <tr><td>1</td><td>Office chairs</td><td class='num'>4</td><td class='num'>$480.00</td></tr>\n"
|
||||
"\n"
|
||||
"Keep ALL HTML tags, CSS, classes, and IDs EXACTLY as-is.\n"
|
||||
"Do NOT modify any <style> blocks, structural HTML, or class names."
|
||||
),
|
||||
),
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=f"Outline/context (data to fill into template):\n{outline_text}",
|
||||
),
|
||||
]
|
||||
if has_logo:
|
||||
messages.append(
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=(
|
||||
"LOGO:\n"
|
||||
"- A company logo is available.\n"
|
||||
"- Insert the exact placeholder text {{LOGO_BLOCK}} once where the logo should appear.\n"
|
||||
"- Do NOT create your own <img> for the logo; only place {{LOGO_BLOCK}}.\n"
|
||||
),
|
||||
)
|
||||
)
|
||||
if additional_instructions and additional_instructions.strip():
|
||||
messages.append(
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=(
|
||||
"ADDITIONAL INSTRUCTIONS (highest priority):\n"
|
||||
f"{additional_instructions.strip()}\n\n"
|
||||
"Apply these instructions while preserving the template structure."
|
||||
),
|
||||
)
|
||||
)
|
||||
messages.append(
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=(
|
||||
f"COMPLETE HTML TEMPLATE (copy ALL of this, replacing ONLY {{{{PLACEHOLDER}}}} tokens):\n\n{template_html}"
|
||||
),
|
||||
)
|
||||
)
|
||||
if draft_sections:
|
||||
sections_payload = [section.model_dump(by_alias=True, exclude_none=True) for section in draft_sections]
|
||||
messages.append(
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=f"Section content (JSON):\n{json.dumps(sections_payload, ensure_ascii=True)}",
|
||||
)
|
||||
)
|
||||
|
||||
stream = stream_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
tag="html_template_fill_stream",
|
||||
log_label="html-template-fill-stream",
|
||||
)
|
||||
yield from stream
|
||||
if stream.error or not stream.chunks:
|
||||
raise RuntimeError(f"AI HTML template fill failed: {stream.error or 'no chunks streamed'}")
|
||||
|
||||
|
||||
def generate_html_with_llm(
|
||||
prompt: str,
|
||||
history: list[models.ChatMessage],
|
||||
document_type: str,
|
||||
template_hint: str | None = None,
|
||||
current_html: str | None = None,
|
||||
structured_brief: str | None = None,
|
||||
) -> str:
|
||||
"""Call the LLM for freeform HTML document generation."""
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=html_system_prompt(document_type, template_hint))
|
||||
]
|
||||
messages.extend(history)
|
||||
messages.extend(html_context_messages(template_hint, current_html, structured_brief))
|
||||
messages.append(models.ChatMessage(role="user", content=prompt))
|
||||
|
||||
logger.info(
|
||||
"[AI] HTML generation model=%s doc_type=%s template_hint=%s",
|
||||
SMART_MODEL,
|
||||
document_type,
|
||||
"yes" if template_hint else "no",
|
||||
)
|
||||
parsed = run_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
models.HtmlResponse,
|
||||
tag="html_generate",
|
||||
)
|
||||
if parsed and parsed.html and parsed.html.strip():
|
||||
logger.info("[AI] Generated HTML preview (first 500 chars):\n%s", parsed.html[:500])
|
||||
return parsed.html
|
||||
|
||||
raise RuntimeError("AI HTML generation failed or returned empty output.")
|
||||
|
||||
|
||||
def generate_html_with_llm_stream(
|
||||
prompt: str,
|
||||
history: list[models.ChatMessage],
|
||||
document_type: str,
|
||||
template_hint: str | None = None,
|
||||
current_html: str | None = None,
|
||||
structured_brief: str | None = None,
|
||||
):
|
||||
"""Stream HTML generation from LLM, yielding chunks as they arrive."""
|
||||
if not STREAMING_ENABLED:
|
||||
full_html = generate_html_with_llm(
|
||||
prompt,
|
||||
history,
|
||||
document_type,
|
||||
template_hint,
|
||||
current_html,
|
||||
structured_brief,
|
||||
)
|
||||
yield full_html
|
||||
return
|
||||
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=html_system_prompt(document_type, template_hint))
|
||||
]
|
||||
messages.extend(history)
|
||||
messages.extend(html_context_messages(template_hint, current_html, structured_brief))
|
||||
messages.append(models.ChatMessage(role="user", content=prompt))
|
||||
|
||||
logger.info(
|
||||
"[AI] Streaming HTML model=%s doc_type=%s template_hint=%s",
|
||||
SMART_MODEL,
|
||||
document_type,
|
||||
"yes" if template_hint else "no",
|
||||
)
|
||||
stream = stream_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
tag="html_stream",
|
||||
log_label="html-stream",
|
||||
)
|
||||
yield from stream
|
||||
|
||||
if stream.error or not stream.chunks:
|
||||
raise RuntimeError(f"AI HTML streaming failed: {stream.error or 'no chunks produced'}")
|
||||
|
||||
|
||||
def edit_html_with_llm_stream(
|
||||
*,
|
||||
document_type: str,
|
||||
base_html: str,
|
||||
instructions: str,
|
||||
):
|
||||
"""Stream an updated full HTML document by editing an existing HTML input."""
|
||||
if not base_html or not base_html.strip():
|
||||
raise RuntimeError("Missing base_html for HTML revision")
|
||||
if not instructions or not instructions.strip():
|
||||
raise RuntimeError("Missing instructions for HTML revision")
|
||||
|
||||
system_prompt = html_edit_system_prompt(document_type)
|
||||
messages: list[models.ChatMessage] = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=f"CURRENT_HTML (you MUST preserve everything not explicitly requested):\n\n{base_html}",
|
||||
),
|
||||
]
|
||||
|
||||
messages.append(
|
||||
models.ChatMessage(
|
||||
role="user",
|
||||
content=f"INSTRUCTIONS:\n{instructions}\n\nReturn ONLY the full updated HTML document.",
|
||||
)
|
||||
)
|
||||
|
||||
stream = stream_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
tag="html_edit_stream",
|
||||
log_label="html-edit-stream",
|
||||
)
|
||||
yield from stream
|
||||
|
||||
if stream.error or not stream.chunks:
|
||||
raise RuntimeError(f"AI HTML edit streaming failed: {stream.error or 'no chunks produced'}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"generate_field_values",
|
||||
"generate_html_with_llm",
|
||||
"generate_html_with_llm_stream",
|
||||
"generate_outline_with_llm",
|
||||
"generate_section_draft",
|
||||
"generate_template_fill_html_stream",
|
||||
"edit_html_with_llm_stream",
|
||||
]
|
||||
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
PostHog analytics tracking for Stirling PDF AI document generation.
|
||||
|
||||
Tracks LLM usage, costs, latency, and document generation metrics for client insights.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from config import (
|
||||
FAST_MODEL,
|
||||
FAST_MODEL_REASONING_EFFORT,
|
||||
FAST_MODEL_TEXT_VERBOSITY,
|
||||
POSTHOG_CLIENT,
|
||||
SMART_MODEL,
|
||||
SMART_MODEL_REASONING_EFFORT,
|
||||
SMART_MODEL_TEXT_VERBOSITY,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def track_event(
|
||||
user_id: str | None,
|
||||
event_name: str,
|
||||
properties: dict[str, Any] | None = None,
|
||||
include_model_settings: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Track a generic event to PostHog.
|
||||
|
||||
Args:
|
||||
user_id: User identifier (can be anonymous ID or user email)
|
||||
event_name: Name of the event (e.g., "document_generated")
|
||||
properties: Additional event properties
|
||||
include_model_settings: Include GPT-5 reasoning/verbosity settings in properties
|
||||
"""
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
try:
|
||||
event_props = properties or {}
|
||||
|
||||
# Add model configuration settings for performance tracking
|
||||
if include_model_settings:
|
||||
event_props.update(
|
||||
{
|
||||
"smart_model": SMART_MODEL,
|
||||
"fast_model": FAST_MODEL,
|
||||
"smart_reasoning_effort": SMART_MODEL_REASONING_EFFORT,
|
||||
"smart_text_verbosity": SMART_MODEL_TEXT_VERBOSITY,
|
||||
"fast_reasoning_effort": FAST_MODEL_REASONING_EFFORT,
|
||||
"fast_text_verbosity": FAST_MODEL_TEXT_VERBOSITY,
|
||||
}
|
||||
)
|
||||
|
||||
POSTHOG_CLIENT.capture(
|
||||
distinct_id=user_id,
|
||||
event=event_name,
|
||||
properties=event_props,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to track PostHog event %s: %s", event_name, exc)
|
||||
|
||||
|
||||
def track_session_created(
|
||||
user_id: str | None,
|
||||
session_id: str,
|
||||
doc_type: str,
|
||||
template_id: str | None = None,
|
||||
has_template: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Track when a new AI document generation session is created.
|
||||
|
||||
Args:
|
||||
user_id: User identifier
|
||||
session_id: Session ID
|
||||
doc_type: Document type
|
||||
template_id: Template ID if using a template
|
||||
has_template: Whether user provided a custom template
|
||||
"""
|
||||
properties = {
|
||||
"session_id": session_id,
|
||||
"doc_type": doc_type,
|
||||
"has_template": has_template,
|
||||
}
|
||||
if template_id:
|
||||
properties["template_id"] = template_id
|
||||
|
||||
# Include model settings to track performance across different configurations
|
||||
track_event(user_id, "session_created", properties, include_model_settings=True)
|
||||
|
||||
|
||||
def shutdown() -> None:
|
||||
"""Gracefully shutdown PostHog client and flush pending events."""
|
||||
try:
|
||||
POSTHOG_CLIENT.shutdown()
|
||||
logger.info("PostHog client shutdown completed")
|
||||
except Exception as exc:
|
||||
logger.warning("Error during PostHog shutdown: %s", exc)
|
||||
@@ -1,819 +0,0 @@
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
from typing import Literal
|
||||
|
||||
from flask import Flask, Response, jsonify, request, send_file, send_from_directory, stream_with_context
|
||||
from flask_cors import CORS
|
||||
from pydantic import BaseModel
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.security import safe_join
|
||||
|
||||
import analytics
|
||||
import models
|
||||
from ai_generation import generate_field_values
|
||||
from briefs import _preprocess_intent
|
||||
from chat_router import classify_chat_route
|
||||
from config import (
|
||||
ASSETS_DIR,
|
||||
FAST_MODEL,
|
||||
FLASK_DEBUG,
|
||||
JAVA_BACKEND_API_KEY,
|
||||
JAVA_REQUEST_TIMEOUT_SECONDS,
|
||||
OUTPUT_DIR,
|
||||
SMART_MODEL,
|
||||
logger,
|
||||
model_max_tokens,
|
||||
)
|
||||
from document_types import detect_document_type
|
||||
from editing import register_edit_routes
|
||||
from editing.decisions import answer_conversational_info
|
||||
from file_processing_agent import ToolCatalogService
|
||||
from html_pdf_utils import compile_html_to_pdf
|
||||
from html_utils import inject_theme
|
||||
from java_client import java_headers, java_url
|
||||
from llm_utils import run_ai
|
||||
from pdf_generator import _DEFAULT_TEMPLATES_DIR, PDFGenerator
|
||||
from pdf_text_editor import convert_pdf_to_text_editor_document
|
||||
from prompts import (
|
||||
generate_all_sections_system_prompt,
|
||||
pdf_qa_system_prompt,
|
||||
section_fill_system_prompt,
|
||||
)
|
||||
from smart_folder_creator import create_smart_folder_config
|
||||
from storage import load_versions
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
_tool_catalog_service = ToolCatalogService()
|
||||
register_edit_routes(app)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def log_job_request_sequence() -> None:
|
||||
job_id = request.headers.get("X-Job-Id")
|
||||
if not job_id:
|
||||
return
|
||||
seq = request.headers.get("X-Job-Seq", "?")
|
||||
total = request.headers.get("X-Job-Total", "?")
|
||||
logger.info("[HTTP] job_id=%s req=%s/%s %s %s", job_id, seq, total, request.method, request.path)
|
||||
|
||||
|
||||
def _json_body[T: BaseModel](model: type[T], request_type: Literal["GET", "POST"] = "POST") -> T:
|
||||
if request_type == "GET":
|
||||
payload = request.args.to_dict()
|
||||
else:
|
||||
payload = request.get_json(silent=True)
|
||||
return model.model_validate(payload or {})
|
||||
|
||||
|
||||
def _java_request_json[T: BaseModel](
|
||||
method: str,
|
||||
path: str,
|
||||
payload: BaseModel | None,
|
||||
response_model: type[T],
|
||||
) -> T:
|
||||
url = java_url(path)
|
||||
data = None
|
||||
headers = java_headers()
|
||||
headers["Content-Type"] = "application/json"
|
||||
if payload is not None:
|
||||
payload_data = payload.model_dump(by_alias=True, exclude_none=True)
|
||||
data = json.dumps(payload_data).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=JAVA_REQUEST_TIMEOUT_SECONDS) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
parsed = json.loads(body) if body else {}
|
||||
return response_model.model_validate(parsed)
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8") if exc.fp else ""
|
||||
logger.error("[JAVA] %s %s failed status=%s detail=%s", method, path, exc.code, detail)
|
||||
raise
|
||||
|
||||
|
||||
def _fetch_ai_session(session_id: str):
|
||||
auth_header = request.headers.get("Authorization")
|
||||
api_key = request.headers.get("X-API-KEY")
|
||||
if auth_header or api_key:
|
||||
path = f"/api/v1/ai/create/sessions/{session_id}"
|
||||
elif JAVA_BACKEND_API_KEY:
|
||||
path = f"/api/v1/ai/create/internal/sessions/{session_id}"
|
||||
else:
|
||||
path = f"/api/v1/ai/create/sessions/{session_id}"
|
||||
return _java_request_json("GET", path, None, models.AISession)
|
||||
|
||||
|
||||
def _update_ai_session(session_id: str, payload: models.JavaUpdateSessionRequest):
|
||||
return _java_request_json(
|
||||
"POST", f"/api/v1/ai/create/internal/sessions/{session_id}/update", payload, models.AISession
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/intent/check", methods=["POST"])
|
||||
def intent_check():
|
||||
payload = _json_body(models.IntentCheckRequest)
|
||||
prompt = payload.prompt
|
||||
history = payload.conversation_history
|
||||
current_pdf_url = payload.current_pdf_url
|
||||
intent = _preprocess_intent(prompt, history, bool(current_pdf_url))
|
||||
response = intent.model_copy(update={"doc_type": intent.document_type, "has_pdf": bool(current_pdf_url)})
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/chat/route", methods=["POST"])
|
||||
def chat_route():
|
||||
payload = _json_body(models.ChatRouteRequest)
|
||||
decision = classify_chat_route(payload)
|
||||
return jsonify(decision.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/chat/create-smart-folder", methods=["POST"])
|
||||
def create_smart_folder():
|
||||
payload = _json_body(models.SmartFolderCreateRequest)
|
||||
result = create_smart_folder_config(payload)
|
||||
return jsonify(result.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/chat/interpret-parameter", methods=["POST"])
|
||||
def interpret_parameter():
|
||||
"""Use AI to interpret an ambiguous parameter response during Tier 2 workflow collection."""
|
||||
payload = _json_body(models.InterpretParameterRequest)
|
||||
system_prompt = (
|
||||
"You are a parameter extraction assistant for a PDF tool workflow.\n"
|
||||
"Given a question asked to the user and the user's response, decide how to interpret it.\n\n"
|
||||
"Return JSON with these fields:\n"
|
||||
'- "type": one of "value", "confused", "cancel", "default"\n'
|
||||
' - "value": the user gave a usable answer, extract it cleanly\n'
|
||||
' - "confused": the user is asking for help or does not understand\n'
|
||||
' - "cancel": the user wants to stop or cancel\n'
|
||||
' - "default": the user is vague; suggest the most sensible default\n'
|
||||
'- "extracted_value": the clean extracted value (only for type="value" or type="default")\n'
|
||||
'- "help_message": a short helpful message explaining what was interpreted or what options exist\n\n'
|
||||
'Be concise. For type="value", extracted_value should be just the raw value (e.g. "English", "3", "DOCX").\n'
|
||||
'For type="default", extracted_value should be the suggested default value.'
|
||||
)
|
||||
user_prompt = (
|
||||
f"Tool: {payload.tool_name}\nQuestion asked: {payload.question}\nUser response: {payload.user_response}"
|
||||
)
|
||||
messages = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=user_prompt),
|
||||
]
|
||||
result = run_ai(
|
||||
FAST_MODEL,
|
||||
messages,
|
||||
models.InterpretParameterResponse,
|
||||
tag="interpret_parameter",
|
||||
log_label="interpret-parameter",
|
||||
)
|
||||
return jsonify(result.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/chat/infer-tools", methods=["POST"])
|
||||
def infer_tools():
|
||||
"""Use AI to infer which PDF tools to apply from a freeform user message."""
|
||||
payload = _json_body(models.InferToolsRequest)
|
||||
tools_list = "\n".join(f"- {t}" for t in payload.available_tools)
|
||||
system_prompt = (
|
||||
"You are a tool selection assistant for Stirling PDF.\n"
|
||||
"Given a user message, identify which PDF tools to apply to achieve the user's goal.\n"
|
||||
"Return tools in the execution order.\n\n"
|
||||
"Rules:\n"
|
||||
"- Only return tools with HIGH confidence — leave out anything uncertain\n"
|
||||
"- Return empty list if the request is a question, ambiguous, or conversational\n"
|
||||
"- Return empty list if the request is about creating a new document\n"
|
||||
"- Maximum 4 tools\n\n"
|
||||
f"Available tools (format: 'id: Display Name'):\n{tools_list}\n\n"
|
||||
'Return JSON: { "tools": [{"tool_id": "...", "confidence": "high"|"medium"|"low"}], "reason": "..." }\n'
|
||||
'IMPORTANT: tool_id must be the exact identifier before the colon (e.g. "convert", not "convert: Convert PDF").'
|
||||
)
|
||||
messages_list = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=f"User message: {payload.message}"),
|
||||
]
|
||||
result = run_ai(
|
||||
FAST_MODEL,
|
||||
messages_list,
|
||||
models.InferToolsResponse,
|
||||
tag="infer_tools",
|
||||
log_label="infer-tools",
|
||||
)
|
||||
filtered = models.InferToolsResponse(
|
||||
tools=[t for t in result.tools if t.confidence == "high"],
|
||||
reason=result.reason,
|
||||
)
|
||||
return jsonify(filtered.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/chat/info", methods=["POST"])
|
||||
def chat_info():
|
||||
"""Handle conversational queries without requiring a file or session."""
|
||||
payload = _json_body(models.ChatInfoRequest)
|
||||
tool_catalog = ToolCatalogService()
|
||||
assistant_message = answer_conversational_info(
|
||||
payload.message,
|
||||
payload.history,
|
||||
tool_catalog,
|
||||
)
|
||||
response = models.ChatInfoResponse(assistant_message=assistant_message)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/detect_type", methods=["POST"])
|
||||
def detect_type():
|
||||
"""
|
||||
Detect document type from a prompt using a fast AI model.
|
||||
|
||||
This endpoint uses a cheap/fast model (FAST_MODEL) for AI classification
|
||||
to minimize cost and latency.
|
||||
|
||||
Request body:
|
||||
- prompt: The user's document request
|
||||
- explicitType: If provided, skip detection and return this type
|
||||
|
||||
Response:
|
||||
- docType: The detected type
|
||||
- confidence: "medium" (AI), "low" (AI)
|
||||
- method: "explicit" or "ai"
|
||||
"""
|
||||
payload = _json_body(models.DetectTypeRequest)
|
||||
prompt = payload.prompt
|
||||
explicit_type = payload.explicit_type or ""
|
||||
|
||||
# If user explicitly provided a type, skip detection
|
||||
if explicit_type and explicit_type not in ("other", "document", "miscellaneous", ""):
|
||||
logger.info("[DETECT] Using explicit doc_type=%s, skipping detection", explicit_type)
|
||||
response = models.DetectTypeResponse(doc_type=explicit_type, confidence="high", method="explicit")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
if not prompt:
|
||||
response = models.DetectTypeResponse(error="Missing prompt for detection")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 400
|
||||
|
||||
# Use fast AI detection (uses FAST_MODEL for cheap classification)
|
||||
ai_type, confidence = detect_document_type(prompt, confidence_threshold=0.7)
|
||||
logger.info("[DETECT] Fast AI detected doc_type=%s (confidence=%.2f)", ai_type, confidence)
|
||||
response = models.DetectTypeResponse(
|
||||
doc_type=ai_type or "other",
|
||||
confidence="medium" if confidence >= 0.7 else "low",
|
||||
method="ai",
|
||||
)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/pdf/answer", methods=["POST"])
|
||||
def pdf_answer():
|
||||
payload = _json_body(models.PdfAnswerRequest)
|
||||
pdf_url = payload.pdf_url
|
||||
question = payload.question
|
||||
if not pdf_url or not question:
|
||||
response = models.PdfAnswerResponse(error="Missing pdfUrl or question")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 400
|
||||
|
||||
filename = os.path.basename(pdf_url.split("?")[0])
|
||||
if not filename.lower().endswith(".pdf"):
|
||||
response = models.PdfAnswerResponse(error="Invalid pdf file")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 400
|
||||
|
||||
pdf_path = safe_join(OUTPUT_DIR, filename)
|
||||
if pdf_path is None or not os.path.exists(pdf_path):
|
||||
response = models.PdfAnswerResponse(error="PDF not found")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 404
|
||||
|
||||
doc = convert_pdf_to_text_editor_document(pdf_path)
|
||||
|
||||
pages = doc.document.pages if doc else []
|
||||
snippets: list[str] = []
|
||||
for page in pages:
|
||||
for elem in page.text_elements:
|
||||
text = elem.text
|
||||
if text:
|
||||
snippets.append(str(text))
|
||||
if not snippets:
|
||||
response = models.PdfAnswerResponse(error="No readable text in PDF")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 400
|
||||
|
||||
# Normalize and limit context
|
||||
context = " ".join(snippets)
|
||||
context = " ".join(context.split()) # normalize whitespace
|
||||
max_context = 10000
|
||||
if len(context) > max_context:
|
||||
context = context[:max_context]
|
||||
|
||||
model_name = SMART_MODEL
|
||||
system_prompt = pdf_qa_system_prompt() + "\nReturn JSON matching the provided schema."
|
||||
user_prompt = f"Question: {question}\n\nPDF text:\n{context}"
|
||||
messages = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=user_prompt),
|
||||
]
|
||||
response = run_ai(
|
||||
model_name,
|
||||
messages,
|
||||
models.PdfAnswer,
|
||||
tag="pdf_answer",
|
||||
max_tokens=220,
|
||||
)
|
||||
answer = response.answer.strip()
|
||||
title_like = re.match(r"^why pdfs|^minimalist|^author:", answer, re.IGNORECASE)
|
||||
normalized_answer = re.sub(r"\s+", " ", answer).strip().lower()
|
||||
normalized_context = re.sub(r"\s+", " ", context).strip().lower()
|
||||
copied_context = bool(normalized_answer) and normalized_answer in normalized_context
|
||||
if title_like or copied_context:
|
||||
response = models.PdfAnswerResponse(error="AI answer was invalid or echoed the source text.")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 500
|
||||
|
||||
response = models.PdfAnswerResponse(answer=answer, mode="model")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/create/sessions", methods=["POST"])
|
||||
def create_session():
|
||||
"""Create a new AI session via the Java backend."""
|
||||
create_session_request = _json_body(models.CreateSessionRequest)
|
||||
|
||||
# Doc type should ideally come from chat router; fall back to detection if needed
|
||||
detection_method = "provided"
|
||||
if create_session_request.doc_type in ("miscellaneous", "other", "document", "unknown", ""):
|
||||
# Fall back to detection if doc_type not provided by chat router
|
||||
logger.info("[SESSION] No doc_type provided, running fallback detection")
|
||||
confidence_threshold = 0.7 # Only accept matches with 70%+ confidence
|
||||
detected_type, confidence = detect_document_type(
|
||||
create_session_request.prompt, confidence_threshold=confidence_threshold
|
||||
)
|
||||
if confidence >= confidence_threshold:
|
||||
logger.info(
|
||||
"[SESSION] Detected doc_type=%s from prompt (confidence=%.2f)",
|
||||
detected_type,
|
||||
confidence,
|
||||
)
|
||||
create_session_request.doc_type = detected_type
|
||||
detection_method = "fallback_detection"
|
||||
else:
|
||||
logger.info(
|
||||
"[SESSION] Detection confidence too low (%.2f < %.2f), using 'other'",
|
||||
confidence,
|
||||
confidence_threshold,
|
||||
)
|
||||
create_session_request.doc_type = "other"
|
||||
detection_method = "fallback_low_confidence"
|
||||
else:
|
||||
logger.info(
|
||||
"[SESSION] Using doc_type=%s from chat router",
|
||||
create_session_request.doc_type,
|
||||
)
|
||||
|
||||
result = _java_request_json(
|
||||
"POST",
|
||||
"/api/v1/ai/create/sessions",
|
||||
create_session_request,
|
||||
models.JavaCreateSessionResponse,
|
||||
)
|
||||
session_id = result.session_id
|
||||
logger.info(
|
||||
"[SESSION] Created session %s doc_type=%s method=%s",
|
||||
session_id,
|
||||
create_session_request.doc_type,
|
||||
detection_method,
|
||||
)
|
||||
# Track session creation in PostHog
|
||||
safe_doc_type = re.sub(r"[^a-zA-Z0-9_]+", "", (create_session_request.doc_type or "").lower())
|
||||
has_html_template = (_DEFAULT_TEMPLATES_DIR / f"{safe_doc_type}.html").exists()
|
||||
analytics.track_session_created(
|
||||
user_id=session_id,
|
||||
session_id=session_id,
|
||||
doc_type=create_session_request.doc_type or "unknown",
|
||||
template_id=create_session_request.template_id,
|
||||
has_template=has_html_template,
|
||||
)
|
||||
|
||||
response = models.CreateSessionResponse(
|
||||
session_id=session_id,
|
||||
doc_type=create_session_request.doc_type,
|
||||
detection_method=detection_method,
|
||||
)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/create/sessions/<session_id>/outline", methods=["POST"])
|
||||
def update_outline(session_id: str):
|
||||
"""Update session with approved outline."""
|
||||
payload = _json_body(models.UpdateOutlineRequest)
|
||||
_java_request_json(
|
||||
"POST",
|
||||
f"/api/v1/ai/create/sessions/{session_id}/outline",
|
||||
payload,
|
||||
models.AISession,
|
||||
)
|
||||
response = models.SuccessResponse(success=True)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/create/sessions/<session_id>/draft", methods=["POST"])
|
||||
def update_draft(session_id: str):
|
||||
"""Update session with approved draft sections."""
|
||||
payload = _json_body(models.UpdateDraftRequest)
|
||||
_java_request_json(
|
||||
"POST",
|
||||
f"/api/v1/ai/create/sessions/{session_id}/draft",
|
||||
payload,
|
||||
models.AISession,
|
||||
)
|
||||
response = models.SuccessResponse(success=True)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/create/sessions/<session_id>/template", methods=["POST"])
|
||||
def update_template(session_id: str):
|
||||
"""Update session template selection."""
|
||||
payload = _json_body(models.UpdateTemplateRequest)
|
||||
_java_request_json(
|
||||
"POST",
|
||||
f"/api/v1/ai/create/sessions/{session_id}/template",
|
||||
payload,
|
||||
models.AISession,
|
||||
)
|
||||
response = models.SuccessResponse(success=True)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/create/sessions/<session_id>/reprompt", methods=["POST"])
|
||||
def reprompt_session(session_id: str):
|
||||
"""Update session with new prompt."""
|
||||
payload = _json_body(models.RepromptRequest)
|
||||
_java_request_json(
|
||||
"POST",
|
||||
f"/api/v1/ai/create/sessions/{session_id}/reprompt",
|
||||
payload,
|
||||
models.AISession,
|
||||
)
|
||||
response = models.SuccessResponse(success=True)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/create/sessions/<session_id>/stream", methods=["POST"])
|
||||
def create_stream(session_id: str):
|
||||
payload = _json_body(models.CreateStreamRequest)
|
||||
session = _fetch_ai_session(session_id)
|
||||
prompt = session.prompt_latest or session.prompt_initial or ""
|
||||
doc_type = session.doc_type or "other"
|
||||
template_id = session.template_id
|
||||
outline_text = session.outline_text or ""
|
||||
outline_filename = session.outline_filename
|
||||
constraints = session.outline_constraints
|
||||
draft_sections = session.draft_sections
|
||||
logo_base64 = payload.theme.logo_base64 if payload.theme else None
|
||||
theme = payload.theme.css_overrides() if payload.theme else None
|
||||
logger.info(
|
||||
"[STREAM] session_id=%s has_theme=%s has_logo=%s",
|
||||
session_id,
|
||||
bool(theme),
|
||||
bool(logo_base64),
|
||||
)
|
||||
base_html = payload.base_html or session.polished_html or None
|
||||
handler = PDFGenerator(
|
||||
session_id=session_id,
|
||||
phase=payload.phase,
|
||||
prompt=prompt,
|
||||
doc_type=doc_type,
|
||||
template_id=template_id,
|
||||
outline_text=outline_text,
|
||||
outline_filename=outline_filename,
|
||||
constraints=constraints,
|
||||
draft_sections=draft_sections,
|
||||
update_session=_update_ai_session,
|
||||
theme=theme,
|
||||
logo_base64=logo_base64,
|
||||
base_html=base_html,
|
||||
instructions=payload.additional_instructions,
|
||||
)
|
||||
|
||||
return Response(
|
||||
stream_with_context(handler.generate()),
|
||||
mimetype="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/create/sessions/<session_id>/fields", methods=["POST"])
|
||||
def fill_fields(session_id: str):
|
||||
logger.info("[AI create] fill_fields session_id=%s", session_id)
|
||||
session = _fetch_ai_session(session_id)
|
||||
|
||||
payload = _json_body(models.FillFieldsRequest)
|
||||
fields = payload.fields
|
||||
extra_prompt = payload.extra_prompt
|
||||
if not isinstance(fields, list):
|
||||
return jsonify({"error": "Fields must be a list"}), 400
|
||||
|
||||
prompt = session.prompt_latest or session.prompt_initial or ""
|
||||
if extra_prompt:
|
||||
prompt = f"{prompt}\n{extra_prompt}"
|
||||
doc_type = session.doc_type or "other"
|
||||
constraints = session.outline_constraints
|
||||
|
||||
filled = generate_field_values(prompt, doc_type, fields, constraints)
|
||||
response = models.FillFieldsResponse(fields=filled)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/generate_section", methods=["POST"])
|
||||
def generate_section():
|
||||
"""Generate content for a single section based on a custom prompt."""
|
||||
payload = _json_body(models.GenerateSectionRequest)
|
||||
_session_id = payload.session_id
|
||||
section_label = payload.section_label
|
||||
section_index = payload.section_index
|
||||
custom_prompt = payload.custom_prompt
|
||||
document_prompt = payload.document_prompt
|
||||
document_type = payload.doc_type
|
||||
existing_sections = payload.existing_sections
|
||||
|
||||
logger.info(
|
||||
"[AI] generate_section label=%s custom_prompt=%s", section_label, custom_prompt[:50] if custom_prompt else ""
|
||||
)
|
||||
|
||||
# Build context from existing sections (don't truncate - AI needs full context for calculations)
|
||||
sections_context = "\n".join([f"- {sec.label}: {sec.value}" for sec in existing_sections if sec.value])
|
||||
system_prompt = section_fill_system_prompt(
|
||||
document_type,
|
||||
document_prompt[:500],
|
||||
sections_context,
|
||||
section_label,
|
||||
custom_prompt,
|
||||
)
|
||||
|
||||
messages = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=f"Generate content for: {section_label}"),
|
||||
]
|
||||
parsed = run_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
models.SectionContent,
|
||||
tag="generate_section",
|
||||
max_tokens=500,
|
||||
)
|
||||
response = models.GenerateSectionResponse(content=parsed.content.strip(), section_index=section_index)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
def _run_sections_batch(
|
||||
batch: list[tuple[int, str]],
|
||||
document_prompt: str,
|
||||
additional_prompt: str | None,
|
||||
system_prompt: str,
|
||||
) -> dict[int, str]:
|
||||
sections_list = "\n".join([f"{i + 1}. {label}" for i, label in batch])
|
||||
user_prompt = (
|
||||
f"User's document request:\n{document_prompt}\n\n"
|
||||
"Generate ONLY these sections (leave others unchanged):\n"
|
||||
f"{sections_list}"
|
||||
)
|
||||
if additional_prompt:
|
||||
user_prompt = f"{user_prompt}\n\nAdditional instructions:\n{additional_prompt}"
|
||||
messages = [
|
||||
models.ChatMessage(role="system", content=system_prompt),
|
||||
models.ChatMessage(role="user", content=user_prompt),
|
||||
]
|
||||
parsed = run_ai(
|
||||
SMART_MODEL,
|
||||
messages,
|
||||
models.LLMGenerateAllSectionsResponse,
|
||||
tag="generate_all_sections",
|
||||
max_tokens=model_max_tokens(SMART_MODEL),
|
||||
)
|
||||
return {section.index - 1: section.value for section in parsed.sections if section.value}
|
||||
|
||||
|
||||
@app.route("/api/generate_all_sections", methods=["POST"])
|
||||
def generate_all_sections():
|
||||
"""Generate content for selected sections."""
|
||||
generation_start = time.time()
|
||||
payload = _json_body(models.GenerateAllSectionsRequest)
|
||||
_session_id = payload.session_id
|
||||
document_prompt = payload.document_prompt
|
||||
document_type = payload.doc_type
|
||||
current_sections = payload.sections
|
||||
only_indices = payload.only_indices # Optional: only generate for these indices
|
||||
additional_prompt = payload.additional_prompt # Optional: extra instructions from user
|
||||
|
||||
# If onlyIndices is provided, only generate for those sections
|
||||
if only_indices is not None:
|
||||
indices_to_generate = set(only_indices)
|
||||
else:
|
||||
indices_to_generate = set(range(len(current_sections)))
|
||||
|
||||
logger.info(
|
||||
"[AI] generate_all_sections doc_type=%s total_sections=%d generating=%d",
|
||||
document_type,
|
||||
len(current_sections),
|
||||
len(indices_to_generate),
|
||||
)
|
||||
|
||||
if not current_sections:
|
||||
response = models.GenerateAllSectionsResponse(error="No sections provided")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True)), 400
|
||||
|
||||
if not indices_to_generate:
|
||||
# Nothing to generate, return sections as-is
|
||||
response = models.GenerateAllSectionsResponse(sections=current_sections)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
# Build the section labels list (only for sections we're generating)
|
||||
section_labels = [section.label for section in current_sections]
|
||||
|
||||
# Build prompt only for sections we want to generate
|
||||
sections_to_generate = [(i, section_labels[i]) for i in sorted(indices_to_generate) if i < len(current_sections)]
|
||||
system_prompt = generate_all_sections_system_prompt(document_type)
|
||||
|
||||
# Batch to avoid hitting model output token limits — each batch is a separate API call.
|
||||
batch_size = 3
|
||||
batches = [sections_to_generate[i : i + batch_size] for i in range(0, len(sections_to_generate), batch_size)]
|
||||
|
||||
generated_content: dict[int, str] = {}
|
||||
failed_indices: list[int] = []
|
||||
|
||||
for batch in batches:
|
||||
try:
|
||||
generated_content.update(_run_sections_batch(batch, document_prompt, additional_prompt, system_prompt))
|
||||
except Exception as batch_err:
|
||||
logger.warning(
|
||||
"[AI] generate_all_sections batch failed (%s), retrying each section individually",
|
||||
batch_err,
|
||||
)
|
||||
for item in batch:
|
||||
try:
|
||||
generated_content.update(
|
||||
_run_sections_batch([item], document_prompt, additional_prompt, system_prompt)
|
||||
)
|
||||
except Exception as single_err:
|
||||
logger.error(
|
||||
"[AI] generate_all_sections failed for section index=%d label=%r: %s",
|
||||
item[0],
|
||||
item[1],
|
||||
single_err,
|
||||
)
|
||||
failed_indices.append(item[0])
|
||||
|
||||
# Build final sections list, keeping existing content for non-generated sections
|
||||
filled_sections = []
|
||||
for i, section in enumerate(current_sections):
|
||||
label = section.label
|
||||
if i in generated_content:
|
||||
# Use newly generated content
|
||||
filled_sections.append(models.DraftSection(label=label, value=generated_content[i]))
|
||||
else:
|
||||
# Keep existing content
|
||||
filled_sections.append(models.DraftSection(label=label, value=section.value))
|
||||
|
||||
generation_duration = (time.time() - generation_start) * 1000
|
||||
|
||||
# Track section generation
|
||||
analytics.track_event(
|
||||
user_id=_session_id or "unknown",
|
||||
event_name="sections_generated",
|
||||
properties={
|
||||
"session_id": _session_id,
|
||||
"doc_type": document_type,
|
||||
"total_sections": len(current_sections),
|
||||
"generated_sections": len(indices_to_generate),
|
||||
"generation_time_ms": generation_duration,
|
||||
"has_additional_prompt": bool(additional_prompt),
|
||||
},
|
||||
)
|
||||
|
||||
response = models.GenerateAllSectionsResponse(
|
||||
sections=filled_sections,
|
||||
incomplete_section_indices=failed_indices if failed_indices else None,
|
||||
)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/output/<path:filename>", methods=["GET"])
|
||||
def serve_output_file(filename: str):
|
||||
"""Serve generated PDF files and stored assets."""
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
try:
|
||||
response = send_from_directory(OUTPUT_DIR, filename, mimetype=mime_type or "application/pdf")
|
||||
file_size = response.headers.get("Content-Length", "unknown")
|
||||
logger.info("[SERVE] Serving file=%s size=%s bytes mime=%s", filename, file_size, mime_type)
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET"
|
||||
return response
|
||||
except NotFound:
|
||||
logger.warning("[SERVE] File not found: %s", filename)
|
||||
return jsonify({"error": "File not found"}), 404
|
||||
|
||||
|
||||
@app.route("/api/versions/<user_id>", methods=["GET"])
|
||||
def list_versions(user_id: str):
|
||||
response = models.VersionsResponse(versions=load_versions(user_id))
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/assets/upload", methods=["POST"])
|
||||
def upload_asset():
|
||||
file = request.files.get("file")
|
||||
if not file:
|
||||
return jsonify({"error": "Missing file"}), 400
|
||||
|
||||
_, ext = os.path.splitext(file.filename or "")
|
||||
ext = ext.lower()
|
||||
if ext not in {".png", ".jpg", ".jpeg", ".gif"}:
|
||||
return jsonify({"error": "Unsupported file type"}), 400
|
||||
|
||||
asset_id = f"{uuid.uuid4().hex}{ext}"
|
||||
output_path = os.path.join(ASSETS_DIR, asset_id)
|
||||
os.makedirs(ASSETS_DIR, exist_ok=True)
|
||||
file.save(output_path)
|
||||
|
||||
response = models.UploadAssetResponse(
|
||||
asset_id=asset_id,
|
||||
asset_url=f"/output/assets/{asset_id}",
|
||||
)
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
@app.route("/api/templates/previews", methods=["GET"])
|
||||
def list_preview_templates():
|
||||
"""Return sorted list of preview template stem names."""
|
||||
stems = sorted(p.stem for p in _DEFAULT_TEMPLATES_DIR.glob("*_preview.html"))
|
||||
return jsonify({"templates": stems})
|
||||
|
||||
|
||||
@app.route("/api/templates/preview-html", methods=["GET"])
|
||||
def get_template_preview_html():
|
||||
"""Return preview template HTML with default theme injected.
|
||||
|
||||
Query param: template — stem name (e.g. 'invoice_preview' or 'invoice_preview.html').
|
||||
The frontend will override CSS vars via JS for live color preview.
|
||||
"""
|
||||
payload = _json_body(models.PreviewTemplateHtmlRequest, "GET")
|
||||
template_param = payload.template
|
||||
if not template_param:
|
||||
return jsonify({"error": "Missing template parameter"}), 400
|
||||
|
||||
# Strip .html extension if present
|
||||
stem = template_param.removesuffix(".html")
|
||||
if "_preview" not in stem:
|
||||
return jsonify({"error": "Template must be a preview template (name must contain '_preview')"}), 400
|
||||
|
||||
html_path = _DEFAULT_TEMPLATES_DIR / f"{stem}.html"
|
||||
if not html_path.exists():
|
||||
return jsonify({"error": f"Template not found: {stem}"}), 404
|
||||
|
||||
html = html_path.read_text(encoding="utf-8", errors="replace")
|
||||
# Inject default theme so vars are defined; frontend will override via JS
|
||||
html = inject_theme(html, None)
|
||||
return Response(html, mimetype="text/html")
|
||||
|
||||
|
||||
@app.route("/api/templates/render-preview", methods=["POST"])
|
||||
def render_preview_to_pdf():
|
||||
"""Render a preview template to PDF with the given theme.
|
||||
|
||||
Body: { template: str, theme: dict | None }
|
||||
Returns: PDF file download.
|
||||
"""
|
||||
payload = _json_body(models.RenderPreviewRequest)
|
||||
template_param = payload.template
|
||||
theme = payload.theme.css_overrides() if payload.theme else None
|
||||
|
||||
if not template_param:
|
||||
return jsonify({"error": "Missing template parameter"}), 400
|
||||
|
||||
stem = template_param.removesuffix(".html")
|
||||
if "_preview" not in stem:
|
||||
return jsonify({"error": "Template must be a preview template"}), 400
|
||||
|
||||
html_path = _DEFAULT_TEMPLATES_DIR / f"{stem}.html"
|
||||
if not html_path.exists():
|
||||
return jsonify({"error": f"Template not found: {stem}"}), 404
|
||||
|
||||
html = html_path.read_text(encoding="utf-8", errors="replace")
|
||||
html = inject_theme(html, theme)
|
||||
|
||||
job_id = f"preview/{uuid.uuid4().hex}"
|
||||
result = compile_html_to_pdf(html, job_id, log_errors=True)
|
||||
|
||||
if result.pdf_path and os.path.exists(result.pdf_path):
|
||||
return send_file(result.pdf_path, mimetype="application/pdf", as_attachment=True, download_name=f"{stem}.pdf")
|
||||
|
||||
return jsonify({"error": result.error or "PDF generation failed"}), 500
|
||||
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
response = models.HealthResponse(status="ok", engine="puppeteer")
|
||||
return jsonify(response.model_dump(by_alias=True, exclude_none=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5001, debug=FLASK_DEBUG)
|
||||
@@ -1,382 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from config import FAST_MODEL, SMART_MODEL
|
||||
from llm_utils import run_ai
|
||||
from models import ChatMessage, IntentCheckResponse, IntentClassification, MissingQuestionsResponse
|
||||
from prompts import brief_missing_info_system_prompt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BRIEF_SCHEMAS: dict[str, dict[str, Any]] = {
|
||||
"resume": {
|
||||
"field_order": [
|
||||
"name",
|
||||
"contact",
|
||||
"location",
|
||||
"target_role",
|
||||
"summary",
|
||||
"work_history",
|
||||
"education",
|
||||
"skills",
|
||||
"achievements",
|
||||
"links",
|
||||
"constraints",
|
||||
],
|
||||
"labels": {
|
||||
"name": ["name", "full name"],
|
||||
"contact": ["contact", "contact info", "contact information", "email/phone"],
|
||||
"location": ["location", "city/country"],
|
||||
"target_role": ["target role", "role", "title", "headline"],
|
||||
"summary": ["summary", "objective", "about"],
|
||||
"work_history": ["experience", "work history", "roles"],
|
||||
"education": ["education", "studies"],
|
||||
"skills": ["skills", "stack"],
|
||||
"achievements": ["achievements", "certifications", "awards"],
|
||||
"links": ["links", "profiles", "linkedin/github"],
|
||||
"constraints": ["constraints", "tone/length/style"],
|
||||
},
|
||||
"questions": {
|
||||
"name": "What's your name as you'd like it on the page?",
|
||||
"contact": "How can someone reach you (email/phone)?",
|
||||
"location": "Where are you based (or remote)?",
|
||||
"target_role": "What role/title and industry are you aiming for?",
|
||||
"summary": "Give me a 1–2 sentence summary about you.",
|
||||
"work_history": "Recent roles: company, title, dates, location, and a few bullets with impact.",
|
||||
"education": "Degree(s), school, and graduation year?",
|
||||
"skills": "Key skills/stack (tech + relevant soft skills)?",
|
||||
"achievements": "Awards/certifications/major achievements?",
|
||||
"links": "Any LinkedIn/GitHub/portfolio links?",
|
||||
"constraints": "Any tone/length constraints (ATS, one-page, etc.)?",
|
||||
},
|
||||
"intro": "Hey! To build a strong resume, you can paste your old resume or just dump everything you remember—name, how to reach you, where you're based, what you're aiming for, your roles, education, skills, links. Share whatever you have and I'll work with it.",
|
||||
},
|
||||
"invoice": {
|
||||
"field_order": [
|
||||
"your_business",
|
||||
"client",
|
||||
"issue_date",
|
||||
"due_date",
|
||||
"line_items",
|
||||
"currency",
|
||||
"payment_terms",
|
||||
"notes",
|
||||
"constraints",
|
||||
],
|
||||
"labels": {
|
||||
"your_business": ["your business", "seller", "from"],
|
||||
"client": ["client", "bill to"],
|
||||
"issue_date": ["issue date", "invoice date"],
|
||||
"due_date": ["due date"],
|
||||
"line_items": ["line items", "services/items"],
|
||||
"currency": ["currency"],
|
||||
"payment_terms": ["payment terms"],
|
||||
"notes": ["notes"],
|
||||
"constraints": ["constraints", "layout/style"],
|
||||
},
|
||||
"questions": {
|
||||
"your_business": "Who is issuing the invoice (business name + contact)?",
|
||||
"client": "Who is being billed (name + contact)?",
|
||||
"issue_date": "Invoice issue date?",
|
||||
"due_date": "Due date?",
|
||||
"line_items": "Line items with description, qty, rate, tax (if any)?",
|
||||
"currency": "Currency?",
|
||||
"payment_terms": "Payment terms and payment methods?",
|
||||
"notes": "Notes to include (late fees, thank you, PO #)?",
|
||||
"constraints": "Branding/layout preferences?",
|
||||
},
|
||||
"intro": "I'll draft an accurate invoice if I know who is billing, who is paying, and the line items. Paste an old invoice or list the details.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def classify_intent_with_llm(prompt: str, history: list[ChatMessage], has_pdf: bool) -> IntentClassification:
|
||||
"""
|
||||
Use a small model to classify intent instead of brittle regex.
|
||||
|
||||
Returns a dict like:
|
||||
{
|
||||
"docType": "invoice|resume|contract|letter|report|form|document",
|
||||
"action": "new|edit|question",
|
||||
"wantsPdf": bool,
|
||||
"hasEnoughInfo": bool,
|
||||
"missingFields": [str],
|
||||
"notes": str
|
||||
}
|
||||
"""
|
||||
system = (
|
||||
"You classify user requests about documents. "
|
||||
"docType must be one of: academic, agenda, brochure, business_card, case_study, checklist, "
|
||||
"contract, creative, datasheet, document, flyer, invoice, letter, manual, menu, minutes, newsletter, "
|
||||
"one_pager, poster, presentation, press_release, proposal, recipe, report, resume, timeline, whitepaper. "
|
||||
"action: 'new' (make/generate), 'edit' (modify existing), 'question' (asking about it). "
|
||||
"wantsPdf: true if they expect/gave permission to generate a PDF. "
|
||||
"hasEnoughInfo: true if there is enough info to proceed without fabricating data. "
|
||||
"missingFields: key details still needed (e.g., for invoice: seller, client, line items; resume: name, contact, work). "
|
||||
"notes: short free-form note. "
|
||||
"Return the classification using the provided schema."
|
||||
)
|
||||
|
||||
conversation = [ChatMessage(role="system", content=system)]
|
||||
# Trim history to keep request small
|
||||
trimmed_history = history[-6:] if len(history) > 6 else history
|
||||
conversation.extend(trimmed_history)
|
||||
conversation.append(ChatMessage(role="user", content=prompt))
|
||||
|
||||
parsed = run_ai(
|
||||
FAST_MODEL,
|
||||
conversation,
|
||||
IntentClassification,
|
||||
tag="intent_classify",
|
||||
max_tokens=20000,
|
||||
)
|
||||
result = parsed
|
||||
logger.info(
|
||||
"[INTENT] llm_classify doc_type=%s action=%s wantsPdf=%s hasEnoughInfo=%s missing=%s notes=%s",
|
||||
result.doc_type,
|
||||
result.action,
|
||||
result.wants_pdf,
|
||||
result.has_enough_info,
|
||||
result.missing_fields,
|
||||
(result.notes or "")[:120],
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _preprocess_intent(
|
||||
prompt: str,
|
||||
history: list[ChatMessage],
|
||||
has_pdf: bool,
|
||||
) -> IntentCheckResponse:
|
||||
"""
|
||||
Lightweight intent classifier used by /api/intent/check.
|
||||
|
||||
It is intentionally heuristic-only to avoid extra model calls. The goal is to
|
||||
decide whether we should proceed with PDF generation and whether it's OK to
|
||||
fabricate placeholder content when the user explicitly asks for it.
|
||||
"""
|
||||
llm = classify_intent_with_llm(prompt, history, has_pdf)
|
||||
return IntentCheckResponse(
|
||||
wants_pdf=llm.wants_pdf,
|
||||
has_enough_info=llm.has_enough_info,
|
||||
document_type=llm.doc_type.value,
|
||||
missing_fields=llm.missing_fields,
|
||||
)
|
||||
|
||||
|
||||
def _extract_structured_fields(text: str, schema: dict[str, Any]) -> dict[str, str]:
|
||||
"""Naively parse user text to pull schema fields."""
|
||||
found: dict[str, str] = {}
|
||||
lower = text.lower()
|
||||
|
||||
if schema.get("field_order") == BRIEF_SCHEMAS["resume"]["field_order"]:
|
||||
work_matches = re.findall(
|
||||
r"(?:experience|work history|role|company|position)\s*[:\-]\s*(.+?)(?=\n\n|\Z)",
|
||||
text,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
if work_matches:
|
||||
found["work_history"] = "\n".join(work_matches[:3])
|
||||
education_matches = re.findall(
|
||||
r"(?:education|degree)\s*[:\-]\s*(.+?)(?=\n\n|\Z)",
|
||||
text,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
if education_matches:
|
||||
found["education"] = "\n".join(education_matches[:2])
|
||||
skills_match = re.search(r"(?:skills|stack)\s*[:\-]\s*(.+)", text, flags=re.IGNORECASE)
|
||||
if skills_match:
|
||||
found["skills"] = skills_match.group(1).strip()
|
||||
|
||||
for field, labels in schema.get("labels", {}).items():
|
||||
for label in labels:
|
||||
pattern = rf"{label}\s*[:\-]\s*(.+?)(?=\n[A-Z][a-zA-Z ]+[:\-]|\Z)"
|
||||
match = re.search(pattern, text, flags=re.IGNORECASE | re.DOTALL)
|
||||
if match:
|
||||
found[field] = match.group(1).strip()
|
||||
break
|
||||
if not found.get("summary") and len(lower) < 200:
|
||||
found["summary"] = text.strip()
|
||||
return {k: v for k, v in found.items() if v}
|
||||
|
||||
|
||||
def _format_missing_message(
|
||||
doc_type: str,
|
||||
schema: dict[str, Any],
|
||||
collected: dict[str, str],
|
||||
missing: list[str],
|
||||
preface: str | None = None,
|
||||
) -> str:
|
||||
"""Fallback text asking the user for missing fields."""
|
||||
intro = schema.get("intro") or f"Need a few details to finish your {doc_type}."
|
||||
lines = [intro]
|
||||
if preface:
|
||||
lines.append(preface)
|
||||
if collected:
|
||||
lines.append("Already have:")
|
||||
for field, value in collected.items():
|
||||
label = schema.get("labels", {}).get(field, [field])[0]
|
||||
lines.append(f"- {label}: {value}")
|
||||
if missing:
|
||||
lines.append("Still need:")
|
||||
questions = schema.get("questions", {})
|
||||
for field in missing[:4]:
|
||||
ask = questions.get(field) or f"{field}?"
|
||||
lines.append(f"- {ask}")
|
||||
lines.append("Partial info is fine—share whatever you remember.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _ai_missing_message(
|
||||
doc_type: str,
|
||||
schema: dict[str, Any],
|
||||
collected: dict[str, str],
|
||||
missing: list[str],
|
||||
) -> str | None:
|
||||
"""Let the model craft clarifying questions when available."""
|
||||
if not missing:
|
||||
return None
|
||||
|
||||
collected_lines = [
|
||||
f"- {schema.get('labels', {}).get(field, [field])[0]}: {value}" for field, value in collected.items()
|
||||
]
|
||||
missing_labels = [schema.get("labels", {}).get(field, [field])[0] for field in missing]
|
||||
user_text = "We already have:\n" + "\n".join(collected_lines) if collected_lines else "We have nothing yet."
|
||||
user_text += "\nNeed to ask for: " + ", ".join(missing_labels)
|
||||
if not collected_lines:
|
||||
user_text += "\nInvite them to paste an old resume or dump all details if they have them."
|
||||
|
||||
system_prompt = brief_missing_info_system_prompt(doc_type) + "\nReturn JSON that matches the provided schema."
|
||||
response = run_ai(
|
||||
SMART_MODEL,
|
||||
[
|
||||
ChatMessage(role="system", content=system_prompt),
|
||||
ChatMessage(role="user", content=user_text),
|
||||
],
|
||||
MissingQuestionsResponse,
|
||||
tag="missing_questions",
|
||||
max_tokens=400,
|
||||
)
|
||||
return response.message
|
||||
|
||||
|
||||
def gather_brief(
|
||||
doc_type: str,
|
||||
prompt: str,
|
||||
history: list[ChatMessage],
|
||||
has_pdf: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Determine whether we have enough structured details to generate.
|
||||
Returns needsInfo + a formatted message when details are missing, or a structured brief.
|
||||
"""
|
||||
classifier = classify_intent_with_llm(prompt, history, has_pdf)
|
||||
if classifier:
|
||||
doc_type = classifier.doc_type.value
|
||||
logger.info(
|
||||
"[BRIEF] using llm doc_type=%s missing=%s hasEnoughInfo=%s wantsPdf=%s",
|
||||
doc_type,
|
||||
classifier.missing_fields,
|
||||
classifier.has_enough_info,
|
||||
classifier.wants_pdf,
|
||||
)
|
||||
|
||||
schema = BRIEF_SCHEMAS.get(doc_type)
|
||||
if not schema:
|
||||
return {"needsInfo": False, "structured_brief": None, "collected": {}, "missing": []}
|
||||
|
||||
user_texts = [entry.content for entry in history if entry.role == "user" and isinstance(entry.content, str)]
|
||||
user_texts.append(prompt or "")
|
||||
combined_text = "\n".join(user_texts)
|
||||
collected = _extract_structured_fields(combined_text, schema)
|
||||
missing = [field for field in schema.get("field_order", []) if field not in collected]
|
||||
if classifier and classifier.missing_fields:
|
||||
# If the model provided missing fields, respect that list.
|
||||
missing = classifier.missing_fields or missing
|
||||
|
||||
def has_minimum_resume(data: dict[str, str]) -> bool:
|
||||
has_name = bool(data.get("name"))
|
||||
has_core = any(data.get(key) for key in ["work_history", "education", "skills", "contact", "target_role"])
|
||||
return has_name and has_core
|
||||
|
||||
def has_minimum_invoice(data: dict[str, str]) -> bool:
|
||||
has_parties = data.get("your_business") and data.get("client")
|
||||
has_items = bool(data.get("line_items"))
|
||||
return bool(has_parties and has_items)
|
||||
|
||||
has_minimum = True
|
||||
if doc_type == "resume":
|
||||
has_minimum = has_minimum_resume(collected)
|
||||
elif doc_type == "invoice":
|
||||
has_minimum = has_minimum_invoice(collected)
|
||||
|
||||
# Never block generation - always proceed with what we have
|
||||
# Users can generate documents even with missing information
|
||||
if False: # Disabled - never block generation
|
||||
preface = None
|
||||
if doc_type == "resume" and not has_minimum:
|
||||
preface = (
|
||||
"I only have a tiny bit so far. I need at least your name plus one of: contact, "
|
||||
"a role snippet, education, skills, or target role."
|
||||
)
|
||||
if doc_type == "invoice" and not has_minimum:
|
||||
preface = "Need who is billing, who is paying, and the line items so I don't invent details."
|
||||
logger.info(
|
||||
"[BRIEF] gating doc_type=%s missing=%s has_minimum=%s",
|
||||
doc_type,
|
||||
missing,
|
||||
has_minimum,
|
||||
)
|
||||
message = _ai_missing_message(doc_type, schema, collected, missing) or _format_missing_message(
|
||||
doc_type, schema, collected, missing, preface=preface
|
||||
)
|
||||
return {
|
||||
"needsInfo": True,
|
||||
"message": message,
|
||||
"collected": collected,
|
||||
"missing": missing,
|
||||
}
|
||||
|
||||
# If fields are missing, let downstream generation know which areas to fill in plausibly
|
||||
# This allows generation to proceed even with missing information
|
||||
fabrication_hint = ""
|
||||
if missing:
|
||||
missing_labels = [schema.get("labels", {}).get(field, [field])[0] for field in missing[:5]]
|
||||
fabrication_hint = (
|
||||
"\n\nIf details are absent, invent plausible, clearly fictional details for: "
|
||||
+ ", ".join(missing_labels)
|
||||
+ "."
|
||||
)
|
||||
|
||||
structured_lines = []
|
||||
for field in schema.get("field_order", []):
|
||||
value = collected.get(field)
|
||||
if value:
|
||||
label = schema.get("labels", {}).get(field, [field])[0]
|
||||
structured_lines.append(f"{label}: {value}")
|
||||
structured_brief = "\n".join(structured_lines)
|
||||
if fabrication_hint:
|
||||
structured_brief = (structured_brief + fabrication_hint).strip()
|
||||
# Always include the full user text - it contains all the details
|
||||
# For detailed prompts, this ensures nothing is lost
|
||||
if combined_text.strip():
|
||||
if structured_brief:
|
||||
structured_brief = (
|
||||
structured_brief + "\n\nCOMPLETE USER INPUT (use ALL information from this):\n" + combined_text
|
||||
).strip()
|
||||
else:
|
||||
# If extraction found nothing, use the full text as the brief
|
||||
structured_brief = combined_text.strip()
|
||||
|
||||
return {
|
||||
"needsInfo": False,
|
||||
"structured_brief": structured_brief or None,
|
||||
"collected": collected,
|
||||
"missing": missing,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["gather_brief", "BRIEF_SCHEMAS", "_preprocess_intent"]
|
||||
@@ -1,47 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from config import FAST_MODEL
|
||||
from format_prompts import FORMAT_PROMPTS
|
||||
from llm_utils import run_ai
|
||||
from models import ChatMessage, ChatRouteRequest, ChatRouteResponse
|
||||
from prompts import chat_route_system_prompt
|
||||
|
||||
|
||||
def classify_chat_route(request: ChatRouteRequest) -> ChatRouteResponse:
|
||||
"""
|
||||
Classify a chat message to route it to edit vs create workflows.
|
||||
|
||||
Returns ChatRouteResponse.
|
||||
"""
|
||||
# Build list of available document types for detection
|
||||
available_types = sorted(list(FORMAT_PROMPTS.keys()))
|
||||
types_list = ", ".join(available_types)
|
||||
system_prompt = chat_route_system_prompt(types_list)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role="system", content=system_prompt),
|
||||
*request.history[-6:],
|
||||
ChatMessage(
|
||||
role="user",
|
||||
content=(
|
||||
"Context:\n"
|
||||
f"- has_files={request.has_files}\n"
|
||||
f"- has_editable_html={request.has_editable_html}\n"
|
||||
f"- has_create_session={request.has_create_session}\n"
|
||||
f"- has_edit_session={request.has_edit_session}\n"
|
||||
f"- last_route={request.last_route}\n\n"
|
||||
f"- request_title={request.request_title}\n"
|
||||
f"- title_context={request.title_context.model_dump() if request.title_context else None}\n\n"
|
||||
f"Message: {request.message}"
|
||||
),
|
||||
),
|
||||
]
|
||||
result = run_ai(
|
||||
FAST_MODEL,
|
||||
messages,
|
||||
ChatRouteResponse,
|
||||
tag="chat_route",
|
||||
log_label="chat-route",
|
||||
log_exchange=True,
|
||||
)
|
||||
return result
|
||||
@@ -1,250 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
import platformdirs
|
||||
from dotenv import load_dotenv
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_openai import ChatOpenAI
|
||||
from posthog import Posthog
|
||||
from posthog.ai.langchain import CallbackHandler
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _get_log_path() -> Path:
|
||||
# Allow explicit override
|
||||
if os.environ["STIRLING_LOG_PATH"]:
|
||||
return Path(os.environ["STIRLING_LOG_PATH"])
|
||||
|
||||
# Check if running in Tauri desktop mode
|
||||
is_tauri = os.environ["STIRLING_PDF_TAURI_MODE"].lower() == "true"
|
||||
|
||||
if is_tauri:
|
||||
# Use OS-native log directory via platformdirs
|
||||
# On Mac: ~/Library/Logs/Stirling-PDF/
|
||||
# On Windows: %LOCALAPPDATA%/Stirling-PDF/Logs/
|
||||
# On Linux: ~/.local/state/Stirling-PDF/log/
|
||||
return Path(platformdirs.user_log_dir("Stirling-PDF"))
|
||||
|
||||
# Server/Docker mode: ./logs/
|
||||
return Path("./logs")
|
||||
|
||||
|
||||
LOG_PATH = _get_log_path()
|
||||
LOG_PATH.mkdir(parents=True, exist_ok=True)
|
||||
LOG_FILE = LOG_PATH / "docgen.log"
|
||||
|
||||
# Create formatters
|
||||
console_formatter = logging.Formatter(
|
||||
"%(asctime)s [%(thread)d] %(levelname)-5s %(name)s - %(message)s", datefmt="%H:%M:%S.%f"
|
||||
)
|
||||
file_formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s [%(thread)d] %(message)s")
|
||||
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# File handler with daily rotation, keeping 14 days
|
||||
file_handler = TimedRotatingFileHandler(
|
||||
LOG_FILE,
|
||||
when="midnight",
|
||||
interval=1,
|
||||
backupCount=14,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Logging to: {LOG_FILE}")
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
|
||||
ASSETS_DIR = os.path.join(OUTPUT_DIR, "assets")
|
||||
DATA_DIR = os.path.join(BASE_DIR, "data")
|
||||
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
|
||||
TEMPLATE_DB_PATH = os.path.join(DATA_DIR, "user_templates.json")
|
||||
VERSIONS_DB_PATH = os.path.join(DATA_DIR, "versions.json")
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
os.makedirs(ASSETS_DIR, exist_ok=True)
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
os.makedirs(TEMPLATE_DIR, exist_ok=True)
|
||||
|
||||
OPENAI_API_KEY = os.environ["STIRLING_OPENAI_API_KEY"]
|
||||
OPENAI_BASE_URL = os.environ["STIRLING_OPENAI_BASE_URL"]
|
||||
ANTHROPIC_API_KEY = os.environ["STIRLING_ANTHROPIC_API_KEY"]
|
||||
JAVA_BACKEND_URL = os.environ["STIRLING_JAVA_BACKEND_URL"]
|
||||
JAVA_BACKEND_API_KEY = os.environ["STIRLING_JAVA_BACKEND_API_KEY"]
|
||||
JAVA_REQUEST_TIMEOUT_SECONDS = float(os.environ["STIRLING_JAVA_REQUEST_TIMEOUT_SECONDS"])
|
||||
|
||||
if not OPENAI_API_KEY and not ANTHROPIC_API_KEY:
|
||||
raise RuntimeError(
|
||||
"Either STIRLING_OPENAI_API_KEY or STIRLING_ANTHROPIC_API_KEY is required to start the AI backend."
|
||||
)
|
||||
SMART_MODEL = os.environ["STIRLING_SMART_MODEL"]
|
||||
FAST_MODEL = os.environ["STIRLING_FAST_MODEL"]
|
||||
|
||||
# GPT-5 reasoning effort configuration
|
||||
# Supported values: minimal, low, medium, high, xhigh
|
||||
# - minimal: Fastest (GPT-5 only)
|
||||
# - low: Speed focused
|
||||
# - medium: Default balance
|
||||
# - high: Quality focused
|
||||
# - xhigh: Maximum quality (GPT-5.2 Pro/Thinking only)
|
||||
SMART_MODEL_REASONING_EFFORT = os.environ["STIRLING_SMART_MODEL_REASONING_EFFORT"]
|
||||
FAST_MODEL_REASONING_EFFORT = os.environ["STIRLING_FAST_MODEL_REASONING_EFFORT"]
|
||||
|
||||
# GPT-5 text verbosity configuration
|
||||
# Supported values: minimal, low, medium, high
|
||||
# Controls output length and detail level
|
||||
SMART_MODEL_TEXT_VERBOSITY = os.environ["STIRLING_SMART_MODEL_TEXT_VERBOSITY"]
|
||||
FAST_MODEL_TEXT_VERBOSITY = os.environ["STIRLING_FAST_MODEL_TEXT_VERBOSITY"]
|
||||
|
||||
FLASK_DEBUG = os.environ["STIRLING_FLASK_DEBUG"] == "1"
|
||||
STREAMING_ENABLED = os.environ["STIRLING_AI_STREAMING"].lower() not in {"0", "false", "no"}
|
||||
if OPENAI_BASE_URL and "ollama" in OPENAI_BASE_URL and not os.environ["STIRLING_AI_STREAMING"]:
|
||||
STREAMING_ENABLED = False
|
||||
PREVIEW_MAX_INFLIGHT = int(os.environ["STIRLING_AI_PREVIEW_MAX_INFLIGHT"])
|
||||
AI_REQUEST_TIMEOUT_SECONDS = float(os.environ["STIRLING_AI_REQUEST_TIMEOUT"])
|
||||
AI_RAW_DEBUG = os.environ["STIRLING_AI_RAW_DEBUG"].lower() not in {"", "0", "false", "no"}
|
||||
AI_MESSAGES_LOG_PATH = LOG_PATH / "ai_messages"
|
||||
AI_MESSAGES_LOG_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def model_max_tokens(model_name: str) -> int:
|
||||
"""
|
||||
Output token limit for a given model.
|
||||
|
||||
This is used by a few routes to avoid provider defaults that are too small for
|
||||
structured outputs. All values can be overridden via env vars.
|
||||
"""
|
||||
if os.environ["STIRLING_AI_MAX_TOKENS"]:
|
||||
return int(os.environ["STIRLING_AI_MAX_TOKENS"])
|
||||
|
||||
if model_name == SMART_MODEL:
|
||||
return int(os.environ["STIRLING_SMART_MODEL_MAX_TOKENS"])
|
||||
if model_name == FAST_MODEL:
|
||||
return int(os.environ["STIRLING_FAST_MODEL_MAX_TOKENS"])
|
||||
if model_name.startswith("claude"):
|
||||
return int(os.environ["STIRLING_CLAUDE_MAX_TOKENS"])
|
||||
|
||||
return int(os.environ["STIRLING_DEFAULT_MODEL_MAX_TOKENS"])
|
||||
|
||||
|
||||
# PostHog Analytics Configuration
|
||||
POSTHOG_API_KEY = os.environ["STIRLING_POSTHOG_API_KEY"]
|
||||
if not POSTHOG_API_KEY:
|
||||
raise RuntimeError("STIRLING_POSTHOG_API_KEY is required to start the AI backend.")
|
||||
POSTHOG_HOST = os.environ["STIRLING_POSTHOG_HOST"]
|
||||
|
||||
# Initialize PostHog client
|
||||
POSTHOG_CLIENT = Posthog(
|
||||
project_api_key=POSTHOG_API_KEY,
|
||||
host=POSTHOG_HOST,
|
||||
)
|
||||
logger.info(f"PostHog analytics enabled: host={POSTHOG_HOST}")
|
||||
|
||||
POSTHOG_CALLBACK = CallbackHandler(client=POSTHOG_CLIENT)
|
||||
|
||||
|
||||
def get_chat_model(
|
||||
model_name: str,
|
||||
streaming: bool = False,
|
||||
max_tokens: int | None = None,
|
||||
model_kwargs: dict | None = None,
|
||||
callbacks: list | None = None,
|
||||
):
|
||||
# Add PostHog callback if enabled and not already in callbacks
|
||||
if callbacks is None or POSTHOG_CALLBACK not in callbacks:
|
||||
callbacks = [POSTHOG_CALLBACK] if callbacks is None else [*callbacks, POSTHOG_CALLBACK]
|
||||
|
||||
# Check if this is a Claude model
|
||||
if model_name.startswith("claude"):
|
||||
if not ANTHROPIC_API_KEY:
|
||||
raise RuntimeError("ANTHROPIC_API_KEY not set")
|
||||
|
||||
kwargs = {
|
||||
"model": model_name,
|
||||
"anthropic_api_key": ANTHROPIC_API_KEY,
|
||||
"streaming": streaming,
|
||||
}
|
||||
if max_tokens is not None:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
if callbacks:
|
||||
kwargs["callbacks"] = callbacks
|
||||
|
||||
return ChatAnthropic(**kwargs)
|
||||
|
||||
# OpenAI/GPT models
|
||||
kwargs = {"model": model_name, "api_key": OPENAI_API_KEY, "streaming": streaming}
|
||||
if max_tokens is not None:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
|
||||
# Build GPT-5 specific parameters if not provided
|
||||
if model_kwargs is None and model_name.startswith("gpt-5"):
|
||||
# Determine which settings to use based on model
|
||||
if model_name == SMART_MODEL:
|
||||
reasoning_effort = SMART_MODEL_REASONING_EFFORT
|
||||
text_verbosity = SMART_MODEL_TEXT_VERBOSITY
|
||||
elif model_name == FAST_MODEL:
|
||||
reasoning_effort = FAST_MODEL_REASONING_EFFORT
|
||||
text_verbosity = FAST_MODEL_TEXT_VERBOSITY
|
||||
else:
|
||||
# Default for other GPT-5 variants
|
||||
reasoning_effort = "medium"
|
||||
text_verbosity = "medium"
|
||||
|
||||
# Pass as explicit parameters instead of model_kwargs to avoid warnings
|
||||
kwargs["reasoning"] = {"effort": reasoning_effort}
|
||||
kwargs["text"] = {"verbosity": text_verbosity}
|
||||
elif model_kwargs:
|
||||
# If custom model_kwargs provided, pass them through
|
||||
kwargs["model_kwargs"] = model_kwargs
|
||||
|
||||
if callbacks:
|
||||
kwargs["callbacks"] = callbacks
|
||||
return ChatOpenAI(**kwargs)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OUTPUT_DIR",
|
||||
"ASSETS_DIR",
|
||||
"DATA_DIR",
|
||||
"TEMPLATE_DIR",
|
||||
"TEMPLATE_DB_PATH",
|
||||
"VERSIONS_DB_PATH",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_BASE_URL",
|
||||
"JAVA_BACKEND_URL",
|
||||
"JAVA_BACKEND_API_KEY",
|
||||
"JAVA_REQUEST_TIMEOUT_SECONDS",
|
||||
"SMART_MODEL",
|
||||
"get_chat_model",
|
||||
"FAST_MODEL",
|
||||
"SMART_MODEL_REASONING_EFFORT",
|
||||
"FAST_MODEL_REASONING_EFFORT",
|
||||
"SMART_MODEL_TEXT_VERBOSITY",
|
||||
"FAST_MODEL_TEXT_VERBOSITY",
|
||||
"FLASK_DEBUG",
|
||||
"STREAMING_ENABLED",
|
||||
"PREVIEW_MAX_INFLIGHT",
|
||||
"AI_REQUEST_TIMEOUT_SECONDS",
|
||||
"AI_RAW_DEBUG",
|
||||
"AI_MESSAGES_LOG_PATH",
|
||||
"POSTHOG_API_KEY",
|
||||
"POSTHOG_HOST",
|
||||
"POSTHOG_CLIENT",
|
||||
"POSTHOG_CALLBACK",
|
||||
"model_max_tokens",
|
||||
]
|
||||
@@ -1,403 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Advisor Agreement</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 130pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Parties ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Legal sections ── */
|
||||
.legal-section {
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.legal-section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.legal-section-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Sub-section ── */
|
||||
.legal-subsection {
|
||||
margin-top: 6pt;
|
||||
margin-bottom: 6pt;
|
||||
margin-left: 10pt;
|
||||
}
|
||||
|
||||
.legal-subsection-heading {
|
||||
font-size: 9pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
/* ── Key-value rows (equity details) ── */
|
||||
.kv-row {
|
||||
display: grid;
|
||||
grid-template-columns: 130pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-top: 4pt;
|
||||
}
|
||||
|
||||
.kv-label {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.kv-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-col { break-inside: avoid; page-break-inside: avoid; }
|
||||
.parties-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.kv-row { break-inside: avoid; page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{AGREEMENT_TITLE}}</div>
|
||||
<div class="doc-subtitle">Advisor Agreement</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">{{AGREEMENT_REF}}</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">{{GOVERNING_LAW}}</span>
|
||||
<span class="meta-label">Jurisdiction:</span> <span class="meta-value">{{JURISDICTION}}</span>
|
||||
<span class="meta-label">Classification:</span> <span class="meta-value">{{CONFIDENTIALITY_LABEL}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Company</div>
|
||||
<div class="party-name">{{COMPANY_LEGAL_NAME}}</div>
|
||||
<div class="party-detail">{{COMPANY_ENTITY_TYPE}}</div>
|
||||
<div class="party-detail">Reg. No.: {{COMPANY_REG_NO}}</div>
|
||||
<div class="party-detail">{{COMPANY_ADDRESS}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Advisor</div>
|
||||
<div class="party-name">{{ADVISOR_LEGAL_NAME}}</div>
|
||||
<div class="party-detail">{{ADVISOR_ENTITY_TYPE}}</div>
|
||||
<div class="party-detail">Reg. No.: {{ADVISOR_REG_NO}}</div>
|
||||
<div class="party-detail">{{ADVISOR_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal sections -->
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Appointment</div>
|
||||
<div class="legal-section-body">The Company appoints the Advisor to provide advisory services to the Company, and the Advisor accepts such appointment, subject to the terms of this Agreement.
|
||||
|
||||
<strong>Role:</strong> {{ADVISOR_ROLE_TITLE}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Services</div>
|
||||
<div class="legal-section-body">{{SERVICES_DESCRIPTION_TEXT}}</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.1 Duties</div>
|
||||
<div class="legal-section-body">{{ADVISOR_DUTIES_ITEMS}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.2 Communications</div>
|
||||
<div class="legal-section-body">{{MEETING_CADENCE_TEXT}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Term</div>
|
||||
<div class="legal-section-body">{{TERM_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Compensation</div>
|
||||
<div class="legal-section-body">{{COMPENSATION_OVERVIEW_TEXT}}</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">4.1 Cash Compensation</div>
|
||||
<div class="legal-section-body">{{CASH_COMPENSATION_TEXT}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">4.2 Equity Compensation</div>
|
||||
<div class="legal-section-body">{{EQUITY_COMPENSATION_TEXT}}</div>
|
||||
<div class="kv-row">
|
||||
<span class="kv-label">Equity type:</span> <span class="kv-value">{{EQUITY_TYPE_TEXT}}</span>
|
||||
<span class="kv-label">Equity amount:</span> <span class="kv-value">{{EQUITY_AMOUNT_TEXT}}</span>
|
||||
<span class="kv-label">Exercise / issue price:</span> <span class="kv-value">{{EQUITY_EXERCISE_PRICE_TEXT}}</span>
|
||||
<span class="kv-label">Vesting:</span> <span class="kv-value">{{VESTING_TEXT}}</span>
|
||||
<span class="kv-label">Post-termination exercise:</span> <span class="kv-value">{{POST_TERMINATION_EXERCISE_TEXT}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">4.3 Expenses</div>
|
||||
<div class="legal-section-body">{{EXPENSES_TEXT}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Confidentiality</div>
|
||||
<div class="legal-section-body">{{CONFIDENTIALITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Intellectual Property and Work Product</div>
|
||||
<div class="legal-section-body">{{IP_ASSIGNMENT_TEXT}}</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">6.1 Works Made for Hire</div>
|
||||
<div class="legal-section-body">{{WORKS_MADE_FOR_HIRE_TEXT}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Conflicts of Interest</div>
|
||||
<div class="legal-section-body">{{CONFLICTS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Non-Solicitation</div>
|
||||
<div class="legal-section-body">{{NON_SOLICIT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. Independent Contractor Status</div>
|
||||
<div class="legal-section-body">{{INDEPENDENT_STATUS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">10. Warranties</div>
|
||||
<div class="legal-section-body">{{WARRANTY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">11. Limitation of Liability</div>
|
||||
<div class="legal-section-body">{{LIABILITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">12. Indemnity</div>
|
||||
<div class="legal-section-body">{{INDEMNITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">13. Termination</div>
|
||||
<div class="legal-section-body">{{TERMINATION_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">14. Notices</div>
|
||||
<div class="legal-section-body">{{NOTICES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">15. Assignment</div>
|
||||
<div class="legal-section-body">{{ASSIGNMENT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">16. Amendments</div>
|
||||
<div class="legal-section-body">{{AMENDMENTS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">17. Entire Agreement</div>
|
||||
<div class="legal-section-body">{{ENTIRE_AGREEMENT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedules -->
|
||||
<div class="section-heading">Schedules</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-body">{{SCHEDULES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">IN WITNESS WHEREOF, the parties have caused this Agreement to be executed by their duly authorised representatives.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Company</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: {{COMPANY_SIGNATORY}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{SIGNATURE_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Advisor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: {{ADVISOR_SIGNATORY}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{SIGNATURE_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,248 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Advisor Agreement — Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { text-align: center; margin-bottom: 8pt; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 130pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 3pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.legal-section { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.legal-section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.legal-section-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.legal-subsection { margin-top: 6pt; margin-bottom: 6pt; margin-left: 10pt; }
|
||||
.legal-subsection-heading { font-size: 9pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.kv-row { display: grid; grid-template-columns: 130pt auto; row-gap: 2pt; margin-top: 4pt; }
|
||||
.kv-label { font-size: 8.5pt; font-weight: 600; color: var(--theme-text-muted); }
|
||||
.kv-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .legal-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<div class="doc-header">
|
||||
<div class="doc-title">Advisor Agreement</div>
|
||||
<div class="doc-subtitle">Advisor Agreement</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">Ref: ADV-2026-01</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">2 February 2026</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">England and Wales</span>
|
||||
<span class="meta-label">Jurisdiction:</span> <span class="meta-value">Courts of England and Wales</span>
|
||||
<span class="meta-label">Classification:</span> <span class="meta-value">Confidential</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Company</div>
|
||||
<div class="party-name">Sterling Systems Group Ltd</div>
|
||||
<div class="party-detail">A private limited company</div>
|
||||
<div class="party-detail">Reg. No.: 12345678</div>
|
||||
<div class="party-detail">100 Kingfisher Way, London, EC2V 7AB, United Kingdom</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Advisor</div>
|
||||
<div class="party-name">Harbourview Strategy Partners Ltd</div>
|
||||
<div class="party-detail">A private limited company</div>
|
||||
<div class="party-detail">Reg. No.: 87654321</div>
|
||||
<div class="party-detail">8 Riverside Walk, London, SE1 9AB, United Kingdom</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Appointment</div>
|
||||
<div class="legal-section-body">The Company appoints the Advisor to provide advisory services to the Company, and the Advisor accepts such appointment, subject to the terms of this Agreement.
|
||||
|
||||
<strong>Role:</strong> Strategic Advisor — Go-to-Market and Partnerships</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Services</div>
|
||||
<div class="legal-section-body">The Advisor will provide strategic guidance and introductions to support the Company's go-to-market execution and partnership development. The Advisor will not have authority to bind the Company and will provide services in an advisory capacity only.</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.1 Duties</div>
|
||||
<div class="legal-section-body">Provide strategic input on target segments, pricing, and positioning.
|
||||
Facilitate introductions to prospective partners and key customers, where appropriate.
|
||||
Review major partnership proposals and provide feedback on terms and structure.
|
||||
Participate in periodic advisory calls and ad hoc consultations as reasonably requested.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.2 Communications</div>
|
||||
<div class="legal-section-body">The parties anticipate one scheduled call per month (30–60 minutes), plus reasonable email support, with additional sessions by mutual agreement.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Term</div>
|
||||
<div class="legal-section-body">This Agreement commences on the Effective Date and continues for twelve (12) months unless earlier terminated in accordance with Section 13. The parties may extend the term by written agreement.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Compensation</div>
|
||||
<div class="legal-section-body">In consideration for the Services, the Company will provide the compensation described below, subject to the terms of this Agreement and any applicable equity plan documents.</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">4.1 Cash Compensation</div>
|
||||
<div class="legal-section-body">No cash compensation will be paid unless agreed in writing in advance for specific projects.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">4.2 Equity Compensation</div>
|
||||
<div class="legal-section-body">The Company will grant the Advisor an equity award under the Company's equity incentive plan, subject to board/committee approval and the definitive equity grant documentation.</div>
|
||||
<div class="kv-row">
|
||||
<span class="kv-label">Equity type:</span> <span class="kv-value">Non-qualified stock options (or equivalent plan award)</span>
|
||||
<span class="kv-label">Equity amount:</span> <span class="kv-value">Options representing 0.25% of fully diluted share capital (on grant date basis)</span>
|
||||
<span class="kv-label">Exercise / issue price:</span> <span class="kv-value">Exercise price per the plan / fair market value on grant date</span>
|
||||
<span class="kv-label">Vesting:</span> <span class="kv-value">24 months vesting with monthly vesting, subject to continued service as advisor</span>
|
||||
<span class="kv-label">Post-termination exercise:</span> <span class="kv-value">90 days to exercise vested options following termination (unless plan states otherwise)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">4.3 Expenses</div>
|
||||
<div class="legal-section-body">Reasonable, pre-approved out-of-pocket expenses will be reimbursed at cost upon receipt of valid supporting documentation.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Confidentiality</div>
|
||||
<div class="legal-section-body">Each party shall keep confidential all non-public information disclosed by the other in connection with this Agreement and shall use such information solely to perform its obligations or exercise its rights under this Agreement.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Intellectual Property and Work Product</div>
|
||||
<div class="legal-section-body">To the extent the Advisor creates any work product, inventions, discoveries, or materials specifically for the Company in the course of providing the Services ("Work Product"), the Advisor hereby assigns to the Company all right, title, and interest in such Work Product upon creation, subject to any pre-existing materials retained by the Advisor.</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">6.1 Works Made for Hire</div>
|
||||
<div class="legal-section-body">Where permitted by law, Work Product shall be deemed "works made for hire" for the Company. To the extent not so deemed, the Advisor assigns all rights in and to the Work Product to the Company as set out above.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Conflicts of Interest</div>
|
||||
<div class="legal-section-body">The Advisor will avoid conflicts of interest and will promptly disclose any actual or potential conflict relating to the Services. The Advisor will not use the Company's confidential information to benefit any third party.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Non-Solicitation</div>
|
||||
<div class="legal-section-body">During the term of this Agreement and for six (6) months thereafter, the Advisor shall not knowingly solicit for employment any employee of the Company with whom the Advisor had material contact in connection with the Services, except through general advertisements not targeted at such employees.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. Independent Contractor Status</div>
|
||||
<div class="legal-section-body">The Advisor is an independent contractor and not an employee, worker, agent, or partner of the Company. The Advisor is responsible for all taxes and statutory obligations arising from compensation under this Agreement.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">10. Warranties</div>
|
||||
<div class="legal-section-body">The Advisor warrants that the Services will be performed with reasonable skill and care. Except as expressly stated, all warranties are excluded to the fullest extent permitted by law.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">11. Limitation of Liability</div>
|
||||
<div class="legal-section-body">Neither party shall be liable for indirect or consequential damages. Each party's aggregate liability arising out of or in connection with this Agreement shall not exceed the total value of compensation paid or payable under this Agreement, except where liability cannot be limited by law.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">12. Indemnity</div>
|
||||
<div class="legal-section-body">Each party will indemnify the other against third-party claims arising from its gross negligence or wilful misconduct in connection with this Agreement, subject to the limitation of liability set out herein.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">13. Termination</div>
|
||||
<div class="legal-section-body">Either party may terminate this Agreement for convenience upon fourteen (14) days' written notice. Either party may terminate immediately for material breach if not cured within fourteen (14) days of written notice. Upon termination, unvested equity (if any) will cease to vest in accordance with the applicable plan and grant documents.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">14. Notices</div>
|
||||
<div class="legal-section-body">Notices must be in writing and delivered by email and/or registered post to the addresses set out in the Parties section (or as updated by notice).</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">15. Assignment</div>
|
||||
<div class="legal-section-body">Neither party may assign this Agreement without the prior written consent of the other, except to an affiliate or in connection with a merger or sale of substantially all assets.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">16. Amendments</div>
|
||||
<div class="legal-section-body">Any amendment to this Agreement must be in writing and signed by both parties.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">17. Entire Agreement</div>
|
||||
<div class="legal-section-body">This Agreement constitutes the entire agreement between the parties regarding its subject matter and supersedes all prior discussions and understandings.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Schedules</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-body">Schedule 1 — Scope of Services and Deliverables (if applicable)
|
||||
Schedule 2 — Equity Award Details (plan, grant date, vesting, and exercise terms)</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">IN WITNESS WHEREOF, the parties have caused this Agreement to be executed by their duly authorised representatives.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Company</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: Alexandra Moore, Director</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 2 February 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Advisor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: Priya Shah, Director</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 2 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,270 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audit Report</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Findings table ── */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 8pt;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.data-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.data-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.data-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.data-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* ── Risk level badges ── */
|
||||
.risk-high {
|
||||
display: inline-block;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
font-weight: 700;
|
||||
font-size: 7.5pt;
|
||||
padding: 1pt 5pt;
|
||||
border-radius: 2pt;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.risk-medium {
|
||||
display: inline-block;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
font-weight: 700;
|
||||
font-size: 7.5pt;
|
||||
padding: 1pt 5pt;
|
||||
border-radius: 2pt;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.risk-low {
|
||||
display: inline-block;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
font-weight: 700;
|
||||
font-size: 7.5pt;
|
||||
padding: 1pt 5pt;
|
||||
border-radius: 2pt;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Sign-off ── */
|
||||
.signoff-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.signoff-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.signoff-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 18pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.data-table tbody tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.signoff-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
.data-table tbody tr { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{REPORT_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}} · Ref: {{AUDIT_REF}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Audit Period:</span> <span class="meta-value">{{AUDIT_PERIOD}}</span>
|
||||
<span class="meta-label">Prepared By:</span> <span class="meta-value">{{PREPARED_BY}}</span>
|
||||
<span class="meta-label">Report Date:</span> <span class="meta-value">{{REPORT_DATE}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<div class="section-heading">Executive Summary</div>
|
||||
<div class="text-section">{{EXECUTIVE_SUMMARY_TEXT}}</div>
|
||||
|
||||
<!-- Scope and Objectives -->
|
||||
<div class="section-heading">Scope and Objectives</div>
|
||||
<div class="text-section">{{SCOPE_TEXT}}</div>
|
||||
|
||||
<!-- Methodology -->
|
||||
<div class="section-heading">Methodology</div>
|
||||
<div class="text-section">{{METHODOLOGY_TEXT}}</div>
|
||||
|
||||
<!-- Findings -->
|
||||
<div class="section-heading">Findings</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%">#</th>
|
||||
<th style="width:30%">Finding</th>
|
||||
<th style="width:12%">Risk Level</th>
|
||||
<th style="width:25%">Recommendation</th>
|
||||
<th style="width:28%">Management Response</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{FINDINGS_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Conclusion -->
|
||||
<div class="section-heading">Conclusion</div>
|
||||
<div class="text-section">{{CONCLUSION_TEXT}}</div>
|
||||
|
||||
<!-- Sign-off -->
|
||||
<div class="section-heading">Sign-Off</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{SIGN_DATE}}</span>
|
||||
</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Lead Auditor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{AUDITOR_NAME}}</div>
|
||||
<div class="sig-field-label">{{AUDITOR_TITLE}}</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Reviewed By</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Authorised Signatory</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audit Report</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.6; margin-bottom: 4pt; }
|
||||
.findings-table { width: 100%; border-collapse: collapse; margin-bottom: 8pt; font-size: 8pt; }
|
||||
.findings-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.findings-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; }
|
||||
.findings-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.findings-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.risk-high { display: inline-block; background: #fee2e2; color: #991b1b; font-weight: 700; font-size: 7.5pt; padding: 1pt 5pt; border-radius: 2pt; white-space: nowrap; }
|
||||
.risk-medium { display: inline-block; background: #fef3c7; color: #92400e; font-weight: 700; font-size: 7.5pt; padding: 1pt 5pt; border-radius: 2pt; white-space: nowrap; }
|
||||
.risk-low { display: inline-block; background: #dcfce7; color: #166534; font-weight: 700; font-size: 7.5pt; padding: 1pt 5pt; border-radius: 2pt; white-space: nowrap; }
|
||||
.signoff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 14pt; }
|
||||
.signoff-col { font-size: 8.5pt; }
|
||||
.signoff-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 18pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .findings-table tbody tr { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Audit Report</div>
|
||||
<div class="doc-subtitle">Vantage Capital Partners Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Audit Period:</span> <span class="meta-value">1 October 2025 – 31 January 2026</span>
|
||||
<span class="meta-label">Prepared By:</span> <span class="meta-value">Dominic Hartley, Head of Internal Audit</span>
|
||||
<span class="meta-label">Report Date:</span> <span class="meta-value">14 February 2026</span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Executive Summary</div>
|
||||
<div class="text-section">This internal audit was conducted to assess the effectiveness of Vantage Capital Partners' procurement and supplier management framework across all UK business units. The audit covered the period 1 October 2025 to 31 January 2026 and encompassed purchase order processes, supplier onboarding controls, contract management and invoice approval workflows.
|
||||
|
||||
Overall, the procurement function operates with a reasonable control environment. However, the audit identified three significant findings and two areas of improvement, as detailed below. Management has acknowledged the findings and proposed remediation actions with target completion dates. The overall audit opinion is Partially Satisfactory.</div>
|
||||
|
||||
<div class="section-heading">Scope and Objectives</div>
|
||||
<div class="text-section">The audit was scoped to cover: (1) purchase order authorisation and segregation of duties; (2) supplier due diligence and onboarding procedures; (3) contract register completeness and renewal monitoring; (4) three-way matching of purchase orders, delivery notes and invoices; and (5) compliance with the Group Procurement Policy (v3.1, updated September 2025).
|
||||
|
||||
Out of scope: IT systems procurement governed by the Technology Steering Committee and capital expenditure projects above £500,000 (subject to separate Board approval processes).
|
||||
|
||||
The objectives were to provide assurance that procurement activities are conducted with appropriate controls, value for money is achieved, and risks of fraud and error are adequately mitigated.</div>
|
||||
|
||||
<div class="section-heading">Methodology</div>
|
||||
<div class="text-section">The audit was conducted in accordance with the Chartered Institute of Internal Auditors (CIIA) Standards. Fieldwork comprised: structured interviews with the Head of Finance Operations, two Procurement Managers and the Legal Counsel; walkthrough testing of the purchase-to-pay process; review of a random sample of 45 purchase orders raised between October 2025 and January 2026; analysis of the supplier register and contract database; and examination of invoice approval audit logs from the Oracle Fusion ERP system.</div>
|
||||
|
||||
<div class="section-heading">Findings</div>
|
||||
<table class="findings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%">#</th>
|
||||
<th style="width:30%">Finding</th>
|
||||
<th style="width:12%">Risk Level</th>
|
||||
<th style="width:25%">Recommendation</th>
|
||||
<th style="width:28%">Management Response</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>11 of 45 purchase orders sampled (24%) lacked a second approver for values between £25,000–£100,000, contrary to the Group Procurement Policy requirement for dual authorisation above £10,000.</td>
|
||||
<td><span class="risk-high">High</span></td>
|
||||
<td>Enforce dual-authorisation controls within Oracle Fusion for all POs above £10,000 and implement automated escalation alerts for non-compliance.</td>
|
||||
<td>Agreed. Finance Operations will reconfigure Oracle Fusion approval workflows by 31 March 2026. A mandatory awareness communication will be issued to all budget holders by 28 February 2026. Owner: Head of Finance Operations.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>The supplier register contained 38 active suppliers with expired due diligence documentation (company credit checks and Modern Slavery Act questionnaires older than 24 months).</td>
|
||||
<td><span class="risk-high">High</span></td>
|
||||
<td>Implement an annual supplier re-accreditation cycle and introduce automated expiry alerts within the supplier management module.</td>
|
||||
<td>Agreed. Procurement will conduct a full supplier re-accreditation exercise by 30 April 2026. Automated alerts will be configured in the ERP system by 31 March 2026. Owner: Group Procurement Manager.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>17 contracts (22% of the active contract register) had no documented renewal or expiry review scheduled, creating a risk of inadvertent auto-renewal on unfavourable terms.</td>
|
||||
<td><span class="risk-medium">Medium</span></td>
|
||||
<td>Assign a contract owner to each active contract and establish a 90-day advance review process within the contract management system.</td>
|
||||
<td>Agreed. Legal Counsel will update the contract register and assign ownership by 31 March 2026. A 90-day review process will be embedded in the contract management system by 30 April 2026. Owner: Legal Counsel.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Invoice coding errors were identified in 6% of sampled invoices, with amounts incorrectly posted to cost centres due to the absence of a mandatory cost-centre validation rule in Oracle Fusion.</td>
|
||||
<td><span class="risk-low">Low</span></td>
|
||||
<td>Introduce a mandatory cost-centre validation rule at invoice entry stage and provide refresher training to accounts payable staff.</td>
|
||||
<td>Agreed. IT will implement the validation rule by 28 February 2026. Training sessions will be arranged for accounts payable by 14 March 2026. Owner: Head of Finance Operations.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-heading">Conclusion</div>
|
||||
<div class="text-section">The audit concludes that the procurement and supplier management function operates with a partially satisfactory control environment. The two high-risk findings relating to purchase order authorisation and supplier due diligence represent the most significant risks and require prompt remediation. Management has responded constructively and the agreed actions, if implemented to the stated deadlines, should substantially improve the control framework. Internal Audit will conduct a follow-up review in July 2026 to verify implementation of all agreed actions.</div>
|
||||
|
||||
<div class="section-heading">Sign-Off</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">14 February 2026</span>
|
||||
</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Lead Auditor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Dominic Hartley</div>
|
||||
<div class="sig-field-label">Head of Internal Audit</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Reviewed By</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Authorised Signatory</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,306 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Board Minutes</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Attendance tables ── */
|
||||
.attendance-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.attendance-table thead tr {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.attendance-table thead th {
|
||||
padding: 4pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-border, #e2e8f0);
|
||||
font-size: 8pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.attendance-table tbody td {
|
||||
padding: 3pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
}
|
||||
|
||||
/* ── Agenda items ── */
|
||||
.agenda-item {
|
||||
margin-bottom: 10pt;
|
||||
padding-left: 8pt;
|
||||
border-left: 2pt solid var(--theme-accent, #2563eb);
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.agenda-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.agenda-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.resolution-box {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
padding: 4pt 6pt;
|
||||
margin-top: 4pt;
|
||||
}
|
||||
|
||||
/* ── Resolutions summary table ── */
|
||||
.resolutions-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.resolutions-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.resolutions-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.resolutions-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.resolutions-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* ── Sign-off ── */
|
||||
.signoff-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.signoff-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.signoff-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 18pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.agenda-item { break-inside: avoid; page-break-inside: avoid; }
|
||||
.signoff-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.resolutions-table tbody tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
.agenda-item { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{DOC_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}} · Ref: {{DOC_REF}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{MEETING_DATE}}</span>
|
||||
<span class="meta-label">Time:</span> <span class="meta-value">{{MEETING_TIME}}</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">{{MEETING_LOCATION}}</span>
|
||||
<span class="meta-label">Chair:</span> <span class="meta-value">{{CHAIR_NAME}}</span>
|
||||
<span class="meta-label">Secretary:</span> <span class="meta-value">{{SECRETARY_NAME}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Present -->
|
||||
<div class="section-heading">Attendance</div>
|
||||
<table class="attendance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Name</th>
|
||||
<th style="width:35%">Title / Role</th>
|
||||
<th style="width:25%">Organisation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{PRESENT_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Apologies -->
|
||||
<div class="section-heading">Apologies</div>
|
||||
<table class="attendance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Name</th>
|
||||
<th style="width:60%">Title / Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{APOLOGIES_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Matters and Business -->
|
||||
<div class="section-heading">Matters and Business of the Meeting</div>
|
||||
{{AGENDA_ITEMS}}
|
||||
|
||||
<!-- Resolutions Summary -->
|
||||
<div class="section-heading">Resolutions Summary</div>
|
||||
<table class="resolutions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:12%">Ref</th>
|
||||
<th style="width:60%">Resolution</th>
|
||||
<th style="width:28%">Carried By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{RESOLUTIONS_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Approval -->
|
||||
<div class="section-heading">Approval</div>
|
||||
<p style="font-size:8.5pt; margin-bottom:10pt;">These minutes are a true and accurate record of the proceedings of the above meeting.</p>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date Approved:</span> <span class="meta-value">{{APPROVAL_DATE}}</span>
|
||||
</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Chair</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{CHAIR_SIGN_NAME}}</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Secretary</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{SECRETARY_SIGN_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,171 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Board Minutes</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.attendance-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.attendance-table thead tr { background: var(--theme-surface); }
|
||||
.attendance-table thead th { padding: 4pt 6pt; text-align: left; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-border); font-size: 8pt; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.attendance-table tbody td { padding: 3pt 6pt; border-bottom: 0.3pt solid var(--theme-border); }
|
||||
.agenda-item { margin-bottom: 10pt; padding-left: 8pt; border-left: 2pt solid var(--theme-accent); page-break-inside: avoid; }
|
||||
.agenda-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.agenda-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 4pt; }
|
||||
.resolution-box { font-size: 8.5pt; font-weight: 600; color: var(--theme-primary); background: var(--theme-surface); border: 0.5pt solid var(--theme-border); padding: 4pt 6pt; margin-top: 4pt; }
|
||||
.resolutions-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.resolutions-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.resolutions-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; }
|
||||
.resolutions-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.resolutions-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.signoff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 14pt; }
|
||||
.signoff-col { font-size: 8.5pt; }
|
||||
.signoff-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 18pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .agenda-item { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Board Minutes</div>
|
||||
<div class="doc-subtitle">Hartwell Group plc</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">12 March 2026</span>
|
||||
<span class="meta-label">Time:</span> <span class="meta-value">09:00 – 11:30</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">Boardroom, 14 Cavendish Square, London W1G 0PH</span>
|
||||
<span class="meta-label">Chair:</span> <span class="meta-value">Dame Catherine Ashworth, Non-Executive Chair</span>
|
||||
<span class="meta-label">Secretary:</span> <span class="meta-value">Richard Tewkesbury, Company Secretary</span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Attendance</div>
|
||||
<table class="attendance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Name</th>
|
||||
<th style="width:35%">Title / Role</th>
|
||||
<th style="width:25%">Organisation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Dame Catherine Ashworth</td><td>Non-Executive Chair</td><td>Hartwell Group plc</td></tr>
|
||||
<tr><td>Jonathan Pemberton</td><td>Chief Executive Officer</td><td>Hartwell Group plc</td></tr>
|
||||
<tr><td>Fiona Caldecott</td><td>Chief Financial Officer</td><td>Hartwell Group plc</td></tr>
|
||||
<tr><td>Marcus Osei-Bonsu</td><td>Chief Operating Officer</td><td>Hartwell Group plc</td></tr>
|
||||
<tr><td>Dr Priya Venkataraman</td><td>Non-Executive Director</td><td>Hartwell Group plc</td></tr>
|
||||
<tr><td>Alastair Drummond</td><td>Senior Independent Director</td><td>Hartwell Group plc</td></tr>
|
||||
<tr><td>Richard Tewkesbury</td><td>Company Secretary</td><td>Hartwell Group plc</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-heading">Apologies</div>
|
||||
<table class="attendance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Name</th>
|
||||
<th style="width:60%">Title / Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Helena Forsythe</td><td>Non-Executive Director</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-heading">Matters and Business of the Meeting</div>
|
||||
|
||||
<div class="agenda-item">
|
||||
<div class="agenda-heading">1. Apologies and Conflicts of Interest</div>
|
||||
<div class="agenda-body">The Chair noted apologies received from Helena Forsythe. No conflicts of interest were declared with respect to items on the agenda. The Chair confirmed that a quorum was present and the meeting was duly constituted.</div>
|
||||
</div>
|
||||
|
||||
<div class="agenda-item">
|
||||
<div class="agenda-heading">2. Minutes of Previous Meeting</div>
|
||||
<div class="agenda-body">The minutes of the Board meeting held on 12 February 2026 (Ref: BM-2026-02) were reviewed and confirmed as a true and accurate record.</div>
|
||||
<div class="resolution-box">Resolution: The Board approved the minutes of the meeting held on 12 February 2026 as a true and accurate record. Proposed by J. Pemberton; seconded by F. Caldecott. Carried unanimously.</div>
|
||||
</div>
|
||||
|
||||
<div class="agenda-item">
|
||||
<div class="agenda-heading">3. Chief Executive's Report</div>
|
||||
<div class="agenda-body">Jonathan Pemberton presented the CEO's report for the period ending 28 February 2026. Revenue performance was 4.2% ahead of budget, driven by stronger-than-anticipated demand in the Infrastructure Services division. The Board noted ongoing recruitment challenges in the Technology division and requested a detailed workforce plan be presented at the April meeting. A tender submission for the Northern Rail Framework contract, valued at approximately £42m over four years, was confirmed as proceeding on schedule.</div>
|
||||
</div>
|
||||
|
||||
<div class="agenda-item">
|
||||
<div class="agenda-heading">4. Financial Performance and Treasury Update</div>
|
||||
<div class="agenda-body">Fiona Caldecott presented the management accounts for February 2026. Group EBITDA stood at £8.3m against a budget of £7.9m. Net debt reduced to £14.1m, reflecting strong cash conversion. The Board approved the revised cash flow forecast for Q2 2026. Fiona noted that the revolving credit facility renewal negotiations with Barclays were progressing well and heads of terms were expected by end of March.</div>
|
||||
<div class="resolution-box">Resolution: The Board approved the revised Q2 2026 cash flow forecast as presented. Carried unanimously.</div>
|
||||
</div>
|
||||
|
||||
<div class="agenda-item">
|
||||
<div class="agenda-heading">5. Any Other Business</div>
|
||||
<div class="agenda-body">Alastair Drummond raised the matter of the forthcoming AGM scheduled for 22 May 2026. Richard Tewkesbury confirmed that notice documents are on track for dispatch by 10 April 2026. The Chair thanked all Directors for their contributions and confirmed the next scheduled Board meeting.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Resolutions Summary</div>
|
||||
<table class="resolutions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:12%">Ref</th>
|
||||
<th style="width:60%">Resolution</th>
|
||||
<th style="width:28%">Carried By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>R-01</td><td>Approval of minutes of meeting held 12 February 2026</td><td>Unanimous</td></tr>
|
||||
<tr><td>R-02</td><td>Approval of revised Q2 2026 cash flow forecast</td><td>Unanimous</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-heading">Approval</div>
|
||||
<p style="font-size:8.5pt; margin-bottom:10pt;">These minutes are a true and accurate record of the proceedings of the above meeting.</p>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date Approved:</span> <span class="meta-value">9 April 2026</span>
|
||||
</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Chair</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Dame Catherine Ashworth</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Secretary</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Richard Tewkesbury</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,274 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Budget Proposal</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin: 8pt 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Tables ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num { text-align: right; }
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Net summary ── */
|
||||
.totals-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.totals-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
min-width: 160pt;
|
||||
}
|
||||
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
|
||||
.totals-table td:first-child {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.totals-table td:last-child {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.totals-table .total-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
border-top: 1pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-top: 4pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.totals-table .net-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Approval block ── */
|
||||
.approval-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 3pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.approval-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.approval-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.totals-wrapper { break-inside: avoid; page-break-inside: avoid; }
|
||||
.totals-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.approval-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{PROPOSAL_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<div>
|
||||
<div class="info-label">Budget Period</div>
|
||||
<div class="info-value">{{BUDGET_PERIOD}}</div>
|
||||
<div class="info-label" style="margin-top:4pt">Prepared By</div>
|
||||
<div class="info-value">{{PREPARED_BY}}</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="info-label">Prepared Date</div>
|
||||
<div class="info-value">{{PREPARED_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<div class="section-heading">Executive Summary</div>
|
||||
<div class="text-section">{{EXECUTIVE_SUMMARY_TEXT}}</div>
|
||||
|
||||
<!-- Revenue Projections -->
|
||||
<div class="section-heading">Revenue Projections</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Revenue Stream</th>
|
||||
<th>Description</th>
|
||||
<th class="num" style="width:60pt">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{REVENUE_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr class="total-row"><td>Total Revenue:</td><td>{{TOTAL_REVENUE}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Expense Breakdown -->
|
||||
<div class="section-heading">Expense Breakdown</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
<th class="num" style="width:60pt">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{EXPENSE_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr class="total-row"><td>Total Expenses:</td><td>{{TOTAL_EXPENSES}}</td></tr>
|
||||
<tr class="net-row"><td>Net Surplus / (Deficit):</td><td>{{NET_SURPLUS}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Assumptions -->
|
||||
<div class="section-heading">Assumptions</div>
|
||||
<div class="text-section">{{ASSUMPTIONS_TEXT}}</div>
|
||||
|
||||
<!-- Approval -->
|
||||
<div class="section-heading">Approval</div>
|
||||
<div class="approval-grid">
|
||||
<span class="approval-label">Approver Name:</span> <span class="approval-value">{{APPROVER_NAME}}</span>
|
||||
<span class="approval-label">Approver Title:</span> <span class="approval-value">{{APPROVER_TITLE}}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,183 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Budget Proposal</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin: 8pt 0; }
|
||||
.info-label { font-weight: 600; font-size: 8.5pt; color: var(--theme-text-muted); }
|
||||
.info-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; white-space: nowrap; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.totals-wrapper { display: flex; justify-content: flex-end; margin-bottom: 10pt; }
|
||||
.totals-table { border-collapse: collapse; font-size: 8.5pt; min-width: 160pt; }
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
.totals-table td:first-child { color: var(--theme-text-muted); }
|
||||
.totals-table td:last-child { text-align: right; }
|
||||
.totals-table .total-row td { font-weight: 700; font-size: 10pt; border-top: 1pt solid var(--theme-primary); padding-top: 4pt; color: var(--theme-heading); }
|
||||
.totals-table .net-row td { font-weight: 700; font-size: 10pt; color: var(--theme-accent); }
|
||||
.approval-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 3pt; margin-bottom: 8pt; }
|
||||
.approval-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.approval-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Budget Proposal</div>
|
||||
<div class="doc-subtitle">Crestwood Technologies Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<div>
|
||||
<div class="info-label">Budget Period</div>
|
||||
<div class="info-value">1 April 2026 – 31 March 2027</div>
|
||||
<div class="info-label" style="margin-top:4pt">Prepared By</div>
|
||||
<div class="info-value">Rachel Thornton, Head of Finance</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="info-label">Prepared Date</div>
|
||||
<div class="info-value">19 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Executive Summary</div>
|
||||
<div class="text-section">This proposal outlines the projected revenue and expenditure for the Product Engineering Division for the financial year ending 31 March 2027. Following strong performance in FY 2025/26, the division is seeking approval for an 18% increase in operating budget to accelerate product development, expand the engineering team, and invest in cloud infrastructure to support anticipated customer growth of 35%.
|
||||
|
||||
The net surplus projection of £412,000 reflects disciplined cost management alongside planned investment in headcount and tooling. All figures are presented in GBP and have been reviewed by the CFO.</div>
|
||||
<div class="section-heading">Revenue Projections</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Revenue Stream</th>
|
||||
<th>Description</th>
|
||||
<th class="num" style="width:60pt">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>SaaS Subscriptions</td>
|
||||
<td>Recurring licence revenue — existing & new customers</td>
|
||||
<td class="num">£2,800,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Professional Services</td>
|
||||
<td>Implementation, training, and consultancy</td>
|
||||
<td class="num">£480,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Marketplace Add-ons</td>
|
||||
<td>Third-party integrations and premium modules</td>
|
||||
<td class="num">£165,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Support Contracts</td>
|
||||
<td>Priority and enterprise support tiers</td>
|
||||
<td class="num">£95,000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr class="total-row"><td>Total Revenue:</td><td>£3,540,000</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Expense Breakdown</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
<th class="num" style="width:60pt">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Headcount</td>
|
||||
<td>Salaries, NI, and pension — 34 FTEs (incl. 4 new hires)</td>
|
||||
<td class="num">£1,980,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cloud Infrastructure</td>
|
||||
<td>AWS hosting, storage, and data services</td>
|
||||
<td class="num">£420,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Software & Tooling</td>
|
||||
<td>Dev tools, SaaS licences, security platforms</td>
|
||||
<td class="num">£215,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Marketing</td>
|
||||
<td>Demand generation, events, content production</td>
|
||||
<td class="num">£180,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>T&E</td>
|
||||
<td>Business travel, client entertainment, conferences</td>
|
||||
<td class="num">£88,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contingency</td>
|
||||
<td>5% operational reserve</td>
|
||||
<td class="num">£165,000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr class="total-row"><td>Total Expenses:</td><td>£3,048,000</td></tr>
|
||||
<tr class="net-row"><td>Net Surplus / (Deficit):</td><td>£492,000</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Assumptions</div>
|
||||
<div class="text-section">1. Revenue growth of 35% year-on-year is based on confirmed pipeline and contracted renewals constituting 72% of the total revenue target.
|
||||
2. Four engineering hires are assumed to be on-boarded by Q1 FY 2026/27; salaries reflect current market rates for mid-senior software engineers in the UK.
|
||||
3. AWS costs include a committed-use discount of 18% negotiated under the existing enterprise agreement.
|
||||
4. All figures exclude capital expenditure, which is budgeted separately under the IT CapEx programme.
|
||||
5. Exchange rate risk on USD-denominated tooling licences is hedged at the treasury level and not reflected here.</div>
|
||||
<div class="section-heading">Approval</div>
|
||||
<div class="approval-grid">
|
||||
<span class="approval-label">Approver Name:</span> <span class="approval-value">Jonathan Crestwood</span>
|
||||
<span class="approval-label">Approver Title:</span> <span class="approval-value">Chief Financial Officer</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,169 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Case Study</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 80pt auto;
|
||||
row-gap: 4pt;
|
||||
margin: 10pt 0;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-bottom: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
padding: 8pt 0;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 700;
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Body text ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Testimonial block ── */
|
||||
.testimonial-block {
|
||||
border-left: 3pt solid var(--theme-accent, #2563eb);
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
padding: 8pt 10pt;
|
||||
margin: 6pt 0;
|
||||
border-radius: 0 3pt 3pt 0;
|
||||
}
|
||||
|
||||
.testimonial-quote {
|
||||
font-size: 9pt;
|
||||
font-style: italic;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.testimonial-attribution {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.testimonial-block { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{CASE_STUDY_TITLE}}</div>
|
||||
<div class="doc-subtitle">Case Study</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Client:</span> <span class="meta-value">{{CLIENT_NAME}}</span>
|
||||
<span class="meta-label">Industry:</span> <span class="meta-value">{{INDUSTRY}}</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{DATE}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client Overview -->
|
||||
<div class="section-heading">Client Overview</div>
|
||||
<div class="text-section">{{CLIENT_OVERVIEW_TEXT}}</div>
|
||||
|
||||
<!-- Challenge -->
|
||||
<div class="section-heading">Challenge</div>
|
||||
<div class="text-section">{{CHALLENGE_TEXT}}</div>
|
||||
|
||||
<!-- Solution -->
|
||||
<div class="section-heading">Solution</div>
|
||||
<div class="text-section">{{SOLUTION_TEXT}}</div>
|
||||
|
||||
<!-- Implementation -->
|
||||
<div class="section-heading">Implementation</div>
|
||||
<div class="text-section">{{IMPLEMENTATION_TEXT}}</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="section-heading">Results</div>
|
||||
<div class="text-section">{{RESULTS_TEXT}}</div>
|
||||
|
||||
<!-- Testimonial -->
|
||||
<div class="section-heading">Client Testimonial</div>
|
||||
<div class="testimonial-block">
|
||||
<div class="testimonial-quote">{{TESTIMONIAL_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Conclusion -->
|
||||
<div class="section-heading">Conclusion</div>
|
||||
<div class="text-section">{{CONCLUSION_TEXT}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,101 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Case Study</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.meta-grid { display: grid; grid-template-columns: 80pt auto; row-gap: 4pt; margin: 10pt 0; border-top: 0.5pt solid var(--theme-border); border-bottom: 0.5pt solid var(--theme-border); padding: 8pt 0; }
|
||||
.meta-label { font-weight: 700; font-size: 9pt; color: var(--theme-text); }
|
||||
.meta-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
.testimonial-block { border-left: 3pt solid var(--theme-accent); background: var(--theme-surface); padding: 8pt 10pt; margin: 6pt 0; border-radius: 0 3pt 3pt 0; }
|
||||
.testimonial-quote { font-size: 9pt; font-style: italic; color: var(--theme-text); line-height: 1.6; margin-bottom: 4pt; white-space: pre-line; }
|
||||
.testimonial-attribution { font-size: 8pt; font-weight: 700; color: var(--theme-primary); }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Case Study</div>
|
||||
<div class="doc-subtitle">Stirling PDF</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div style="font-size: 11pt; font-weight: 600; color: var(--theme-heading); margin: 8pt 0 10pt;">How Nexus Cloud Reduced Invoice Processing Time by 74%</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Client:</span> <span class="meta-value">Nexus Cloud Ltd</span>
|
||||
<span class="meta-label">Industry:</span> <span class="meta-value">Financial Technology (FinTech)</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">February 2026</span>
|
||||
</div>
|
||||
<div class="section-heading">Client Overview</div>
|
||||
<div class="text-section">Nexus Cloud Ltd is a London-based financial technology company providing accounts-payable automation software to mid-market businesses across the UK and Ireland. Founded in 2017, the company employs 210 staff and processes over 4 million invoices annually on behalf of its 320 enterprise clients. Following a Series B funding round in 2024, Nexus Cloud entered a rapid growth phase, scaling its customer base by 45% in eighteen months.</div>
|
||||
<div class="section-heading">Challenge</div>
|
||||
<div class="text-section">Rapid customer growth exposed significant bottlenecks in Nexus Cloud's internal document management and PDF generation infrastructure. The legacy system — a patchwork of third-party libraries and manual export workflows — was unable to scale with demand. Key problems included:
|
||||
|
||||
- Invoice and statement generation times averaging 8.2 seconds per document, causing SLA breaches during peak periods
|
||||
- No support for branded, client-configurable document templates, resulting in high-value customers receiving generic output
|
||||
- A manual quality-assurance step requiring three finance staff to review approximately 600 documents per day
|
||||
- An inability to generate documents programmatically via API, blocking integrations with downstream ERP systems
|
||||
|
||||
The backlog had grown to 12,000 unprocessed documents at peak, and customer satisfaction scores related to document delivery had fallen to 61%.</div>
|
||||
<div class="section-heading">Solution</div>
|
||||
<div class="text-section">Nexus Cloud partnered with Stirling PDF to deploy a fully integrated document generation and processing pipeline. The solution centred on Stirling PDF's AI document creation capabilities, enabling Nexus Cloud to:
|
||||
|
||||
- Define and version-control a library of 24 branded document templates covering invoices, statements, remittance advice, and credit notes
|
||||
- Generate PDF documents programmatically via a REST API with average end-to-end latency of 2.1 seconds
|
||||
- Apply client-specific theming — including custom fonts, colour schemes, and logos — at the point of generation without manual intervention
|
||||
- Route completed documents directly to Nexus Cloud's distribution service via webhook, eliminating the manual QA step for standard document types</div>
|
||||
<div class="section-heading">Implementation</div>
|
||||
<div class="text-section">The implementation was completed in three phases over eight weeks:
|
||||
|
||||
Phase 1 (Weeks 1–3): Template Migration. Nexus Cloud's design team worked with Stirling PDF's onboarding engineers to convert the 24 legacy document designs into Stirling PDF's HTML-based template format. All templates were validated against a sample of 500 historical documents.
|
||||
|
||||
Phase 2 (Weeks 4–6): API Integration. Nexus Cloud's engineering team integrated the Stirling PDF generation API into their existing invoice processing microservice. A staging environment with synthetic load testing at 200 concurrent requests confirmed that latency remained below 2.5 seconds at the 95th percentile.
|
||||
|
||||
Phase 3 (Weeks 7–8): Parallel Running and Cutover. Both systems ran in parallel for two weeks. A total of 38,000 documents were validated side-by-side. No material discrepancies were identified. The legacy system was decommissioned on 14 October 2025.</div>
|
||||
<div class="section-heading">Results</div>
|
||||
<div class="text-section">Within 90 days of full deployment, Nexus Cloud reported the following outcomes:
|
||||
|
||||
- Document generation time reduced from 8.2 seconds to 2.1 seconds — a 74% improvement
|
||||
- Manual QA headcount reallocated from document review to higher-value tasks; estimated annual saving of £87,000 in operational cost
|
||||
- Zero SLA breaches in November and December 2025 (versus 14 in the same period the prior year)
|
||||
- Customer satisfaction score for document delivery increased from 61% to 89%
|
||||
- 12 enterprise clients activated custom-branded templates within the first 60 days, generating an estimated £34,000 in incremental upsell revenue for Nexus Cloud
|
||||
- The document backlog was eliminated within the first week of operation</div>
|
||||
<div class="section-heading">Client Testimonial</div>
|
||||
<div class="testimonial-block">
|
||||
<div class="testimonial-quote">"Switching to Stirling PDF was one of the smoothest technology transitions we have made. The onboarding team understood our constraints from day one, and the API integration took our engineers less than a week. The impact on our document throughput and customer feedback has been immediate and measurable. We are already planning to extend the platform to cover our client-facing reporting suite in Q2 2026."</div>
|
||||
<div class="testimonial-attribution">— Marcus Webb, VP of Engineering, Nexus Cloud Ltd</div>
|
||||
</div>
|
||||
<div class="section-heading">Conclusion</div>
|
||||
<div class="text-section">Nexus Cloud's experience demonstrates how organisations experiencing rapid growth can use Stirling PDF to eliminate document processing bottlenecks without sacrificing quality or brand consistency. By replacing a fragile legacy stack with a scalable, API-first generation platform, Nexus Cloud recovered significant operational capacity, improved customer satisfaction, and created new commercial opportunities through branded document customisation. The project delivered full return on investment within eleven weeks of go-live.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,224 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Committee Agenda</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Attendees table ── */
|
||||
.attendees-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.attendees-table thead tr {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.attendees-table thead th {
|
||||
padding: 4pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-border, #e2e8f0);
|
||||
font-size: 8pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.attendees-table tbody td {
|
||||
padding: 3pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
}
|
||||
|
||||
/* ── Agenda items table ── */
|
||||
.agenda-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 8pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.agenda-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.agenda-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agenda-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.agenda-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.agenda-table .item-num {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.agenda-table tbody tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Agenda</div>
|
||||
<div class="doc-subtitle">{{COMMITTEE_NAME}} · {{ORG_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{MEETING_DATE}}</span>
|
||||
<span class="meta-label">Time:</span> <span class="meta-value">{{MEETING_TIME}}</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">{{MEETING_LOCATION}}</span>
|
||||
<span class="meta-label">Chair:</span> <span class="meta-value">{{CHAIR_NAME}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Expected Attendees -->
|
||||
<div class="section-heading">Expected Attendees</div>
|
||||
<table class="attendees-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Name</th>
|
||||
<th style="width:35%">Title / Role</th>
|
||||
<th style="width:25%">Organisation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ATTENDEES_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Agenda Items -->
|
||||
<div class="section-heading">Agenda Items</div>
|
||||
<table class="agenda-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:8%">#</th>
|
||||
<th style="width:47%">Topic</th>
|
||||
<th style="width:25%">Presenter</th>
|
||||
<th style="width:20%">Time Allotted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{AGENDA_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Documents / Papers Circulated -->
|
||||
<div class="section-heading">Documents and Papers Circulated</div>
|
||||
<div class="text-section">{{DOCUMENTS_TEXT}}</div>
|
||||
|
||||
<!-- Any Other Business -->
|
||||
<div class="section-heading">Any Other Business (AOB)</div>
|
||||
<div class="text-section">{{AOB_TEXT}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,128 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Committee Agenda</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.attendees-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.attendees-table thead tr { background: var(--theme-surface); }
|
||||
.attendees-table thead th { padding: 4pt 6pt; text-align: left; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-border); font-size: 8pt; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.attendees-table tbody td { padding: 3pt 6pt; border-bottom: 0.3pt solid var(--theme-border); }
|
||||
.agenda-table { width: 100%; border-collapse: collapse; margin-bottom: 8pt; font-size: 8.5pt; }
|
||||
.agenda-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.agenda-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; }
|
||||
.agenda-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.agenda-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.agenda-table .item-num { text-align: center; font-weight: 700; color: var(--theme-primary); white-space: nowrap; }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; margin-bottom: 4pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Committee Agenda</div>
|
||||
<div class="doc-subtitle">Meridian Healthcare NHS Foundation Trust</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">26 March 2026</span>
|
||||
<span class="meta-label">Time:</span> <span class="meta-value">14:00 – 16:30</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">Committee Room 3, Trust Headquarters, Bristol BS1 5TT</span>
|
||||
<span class="meta-label">Chair:</span> <span class="meta-value">Sir Geoffrey Harwood, Non-Executive Director</span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Expected Attendees</div>
|
||||
<table class="attendees-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Name</th>
|
||||
<th style="width:35%">Title / Role</th>
|
||||
<th style="width:25%">Organisation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Sir Geoffrey Harwood</td><td>Non-Executive Director (Chair)</td><td>Meridian Healthcare NHS FT</td></tr>
|
||||
<tr><td>Dr Anita Nwachukwu</td><td>Non-Executive Director</td><td>Meridian Healthcare NHS FT</td></tr>
|
||||
<tr><td>Peter Sloane</td><td>Director of Finance</td><td>Meridian Healthcare NHS FT</td></tr>
|
||||
<tr><td>Carolyn Bridges</td><td>Head of Internal Audit</td><td>Meridian Healthcare NHS FT</td></tr>
|
||||
<tr><td>James Firth</td><td>Deputy Director of Governance</td><td>Meridian Healthcare NHS FT</td></tr>
|
||||
<tr><td>Sarah Quilter</td><td>Engagement Lead</td><td>Grant Thornton UK LLP (External Auditors)</td></tr>
|
||||
<tr><td>Helen Mcallister</td><td>Committee Secretary</td><td>Meridian Healthcare NHS FT</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-heading">Agenda Items</div>
|
||||
<table class="agenda-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:8%">#</th>
|
||||
<th style="width:47%">Topic</th>
|
||||
<th style="width:25%">Presenter</th>
|
||||
<th style="width:20%">Time Allotted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class="item-num">1</td><td>Welcome, Apologies and Declarations of Interest</td><td>Chair</td><td>5 mins</td></tr>
|
||||
<tr><td class="item-num">2</td><td>Minutes of Previous Meeting (29 January 2026) and Matters Arising</td><td>Chair / Secretary</td><td>10 mins</td></tr>
|
||||
<tr><td class="item-num">3</td><td>External Audit: Progress Report and Planned Approach for 2025/26 Accounts</td><td>S. Quilter</td><td>20 mins</td></tr>
|
||||
<tr><td class="item-num">4</td><td>Internal Audit: Q3 Progress Report and Tracker of Open Recommendations</td><td>C. Bridges</td><td>25 mins</td></tr>
|
||||
<tr><td class="item-num">5</td><td>Counter-Fraud Annual Update and Referral Summary</td><td>C. Bridges</td><td>15 mins</td></tr>
|
||||
<tr><td class="item-num">6</td><td>Finance Report: Month 11 Management Accounts</td><td>P. Sloane</td><td>20 mins</td></tr>
|
||||
<tr><td class="item-num">7</td><td>Board Assurance Framework: Risk Register Review</td><td>J. Firth</td><td>20 mins</td></tr>
|
||||
<tr><td class="item-num">8</td><td>Committee Effectiveness Self-Assessment – 2025/26 Results</td><td>Chair</td><td>15 mins</td></tr>
|
||||
<tr><td class="item-num">9</td><td>Any Other Business</td><td>Chair</td><td>5 mins</td></tr>
|
||||
<tr><td class="item-num">10</td><td>Date of Next Meeting</td><td>Secretary</td><td>5 mins</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-heading">Documents and Papers Circulated</div>
|
||||
<div class="text-section">The following papers have been circulated in advance of the meeting and should be read prior to attendance:
|
||||
|
||||
1. Draft minutes of the Audit and Risk Committee meeting held 29 January 2026
|
||||
2. External Audit Progress Report – Grant Thornton UK LLP (March 2026)
|
||||
3. Internal Audit Q3 Progress Report and Recommendations Tracker
|
||||
4. Counter-Fraud Annual Summary Report 2025/26
|
||||
5. Month 11 Management Accounts (February 2026)
|
||||
6. Board Assurance Framework – Quarterly Risk Register Update
|
||||
7. Committee Effectiveness Self-Assessment Summary Report
|
||||
|
||||
Papers are accessible via the Board portal. Hard copies available from the Committee Secretary on request.</div>
|
||||
|
||||
<div class="section-heading">Any Other Business (AOB)</div>
|
||||
<div class="text-section">Members wishing to raise items under AOB should notify the Committee Secretary (h.mcallister@meridianhealthcare.nhs.uk) no later than 24 hours before the meeting. The next scheduled meeting of the Audit and Risk Committee is Thursday, 28 May 2026 at 14:00.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,201 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Employee Handbook</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 100pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading (numbered, primary colour) ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 12pt 0 5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Sub-section heading ── */
|
||||
.subsection-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin: 8pt 0 3pt;
|
||||
}
|
||||
|
||||
/* ── Body text ── */
|
||||
.text-section {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 6pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.subsection-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{HANDBOOK_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Handbook meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Version:</span> <span class="meta-value">{{VERSION}}</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- 1. Welcome -->
|
||||
<div class="section-heading">1.0 Welcome</div>
|
||||
<div class="text-section">{{WELCOME_TEXT}}</div>
|
||||
|
||||
<!-- 2. Employment Policies -->
|
||||
<div class="section-heading">2.0 Employment Policies</div>
|
||||
<div class="text-section">{{EMPLOYMENT_POLICIES_TEXT}}</div>
|
||||
|
||||
<!-- 3. Code of Conduct -->
|
||||
<div class="section-heading">3.0 Code of Conduct</div>
|
||||
<div class="text-section">{{CODE_OF_CONDUCT_TEXT}}</div>
|
||||
|
||||
<!-- 4. Benefits Overview -->
|
||||
<div class="section-heading">4.0 Benefits Overview</div>
|
||||
<div class="text-section">{{BENEFITS_TEXT}}</div>
|
||||
|
||||
<!-- 5. Leave Policies -->
|
||||
<div class="section-heading">5.0 Leave Policies</div>
|
||||
<div class="text-section">{{LEAVE_POLICIES_TEXT}}</div>
|
||||
|
||||
<!-- 6. Disciplinary Procedure -->
|
||||
<div class="section-heading">6.0 Disciplinary Procedure</div>
|
||||
<div class="text-section">{{DISCIPLINARY_TEXT}}</div>
|
||||
|
||||
<!-- 7. Acknowledgement -->
|
||||
<div class="section-heading">7.0 Acknowledgement</div>
|
||||
<div class="text-section">I, the undersigned, acknowledge that I have received, read, and understood the contents of this Employee Handbook. I agree to comply with all policies and procedures set out herein.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{EMPLOYEE_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{EMPLOYEE_SIGN_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">HR Representative</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{HR_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{HR_SIGN_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,151 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Employee Handbook</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 3pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 9pt; }
|
||||
.meta-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 12pt 0 5pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 9pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 6pt; white-space: pre-line; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Employee Handbook</div>
|
||||
<div class="doc-subtitle">Northgate Engineering Services Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Version:</span> <span class="meta-value">3.2</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">1 January 2026</span>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">1.0 Welcome</div>
|
||||
<div class="text-section">Welcome to Northgate Engineering Services Ltd. We are delighted that you have joined our team and hope that your time with us will be both rewarding and fulfilling.
|
||||
|
||||
This handbook sets out the key policies, procedures, and standards that govern our working environment. It is intended to give you a clear picture of what you can expect from us and what we expect from you. Please read it carefully and keep it for reference throughout your employment.
|
||||
|
||||
This handbook does not form part of your contract of employment. The company reserves the right to amend any policy set out herein at any time, subject to appropriate consultation. The most current version will always be available on the HR intranet portal.</div>
|
||||
|
||||
<div class="section-heading">2.0 Employment Policies</div>
|
||||
<div class="text-section"><strong>2.1 Equal Opportunities</strong>
|
||||
Northgate Engineering Services is committed to providing equal opportunities for all employees and job applicants. We do not discriminate on the grounds of age, disability, gender reassignment, marriage and civil partnership, pregnancy and maternity, race, religion or belief, sex, or sexual orientation.
|
||||
|
||||
<strong>2.2 Probationary Period</strong>
|
||||
All new employees are subject to a six-month probationary period. During this time, performance and conduct will be monitored closely. The company may extend the probationary period or terminate employment if performance or conduct does not meet the required standard.
|
||||
|
||||
<strong>2.3 Working Hours</strong>
|
||||
Core working hours are Monday to Friday, 08:30 to 17:00, with a 30-minute unpaid lunch break. Flexible working arrangements may be agreed in writing with your line manager and HR.
|
||||
|
||||
<strong>2.4 Pay and Salary Review</strong>
|
||||
Salaries are paid monthly by BACS transfer on the last working day of each month. Annual salary reviews take place in April each year; any increase is discretionary and subject to individual performance and business conditions.</div>
|
||||
|
||||
<div class="section-heading">3.0 Code of Conduct</div>
|
||||
<div class="text-section">All employees are expected to conduct themselves with professionalism, honesty, and integrity at all times. The following behaviours are required:
|
||||
|
||||
- Treat all colleagues, clients, and business partners with courtesy and respect.
|
||||
- Maintain confidentiality in relation to company information, client data, and personal data of colleagues.
|
||||
- Avoid any actual or perceived conflicts of interest; disclose potential conflicts to your line manager promptly.
|
||||
- Use company assets, including IT systems, vehicles, and equipment, only for legitimate business purposes.
|
||||
- Comply with all applicable laws, regulations, and company policies.
|
||||
- Do not make unauthorised commitments or enter into contracts on behalf of the company.
|
||||
|
||||
Breaches of the code of conduct may result in disciplinary action up to and including dismissal.</div>
|
||||
|
||||
<div class="section-heading">4.0 Benefits Overview</div>
|
||||
<div class="text-section">Northgate Engineering Services offers a competitive benefits package, which includes:
|
||||
|
||||
- <strong>Pension:</strong> Employer contribution of 6% into a qualifying workplace pension scheme; employee minimum contribution of 3%.
|
||||
- <strong>Private Medical Insurance:</strong> Individual cover provided through AXA Health from the date of joining; family cover available at a subsidised premium.
|
||||
- <strong>Life Assurance:</strong> Death-in-service benefit of four times annual base salary.
|
||||
- <strong>Annual Leave:</strong> 25 days per year, rising to 28 days after five years' continuous service, plus UK public holidays.
|
||||
- <strong>Cycle to Work:</strong> Interest-free salary sacrifice scheme for bicycles and cycling equipment.
|
||||
- <strong>Employee Assistance Programme:</strong> Confidential 24/7 support line covering mental health, financial, and legal advice.
|
||||
- <strong>Professional Development:</strong> Support for relevant professional qualifications and membership fees, subject to line manager approval.</div>
|
||||
|
||||
<div class="section-heading">5.0 Leave Policies</div>
|
||||
<div class="text-section"><strong>5.1 Annual Leave</strong>
|
||||
Annual leave must be agreed in advance with your line manager and booked via the HR system. A maximum of five days may be carried over into the next leave year; carried-over days expire on 31 March.
|
||||
|
||||
<strong>5.2 Sickness Absence</strong>
|
||||
If you are unable to attend work due to illness, you must notify your line manager by telephone before your normal start time on the first day of absence. A self-certification form must be completed for absences of up to seven calendar days. A fit note from a GP is required for any absence exceeding seven calendar days.
|
||||
|
||||
<strong>5.3 Maternity, Paternity, and Shared Parental Leave</strong>
|
||||
The company provides statutory maternity pay and leave entitlements in accordance with current UK legislation. Enhanced maternity pay of 90% of average weekly earnings is provided for the first 16 weeks for employees with at least one year's continuous service. Please contact HR for full details.
|
||||
|
||||
<strong>5.4 Other Leave</strong>
|
||||
Compassionate leave of up to five days with full pay may be granted on the death of an immediate family member. Jury service leave will be granted; the company will make up the difference between jury service allowance and normal pay for a maximum of 10 working days.</div>
|
||||
|
||||
<div class="section-heading">6.0 Disciplinary Procedure</div>
|
||||
<div class="text-section">The company's disciplinary procedure is designed to ensure that all employees are treated fairly and consistently when concerns arise about conduct or performance. The procedure follows these stages:
|
||||
|
||||
<strong>Stage 1 — Informal Discussion:</strong> For minor issues, the line manager will hold an informal discussion to address the concern and agree on corrective action.
|
||||
|
||||
<strong>Stage 2 — Formal Investigation:</strong> Where informal resolution is not possible, or where the matter is sufficiently serious, a formal investigation will be conducted. The employee will be notified in writing and invited to a disciplinary hearing with appropriate notice.
|
||||
|
||||
<strong>Stage 3 — Disciplinary Hearing:</strong> The employee has the right to be accompanied by a trade union representative or work colleague. Following the hearing, the outcome may be: no further action, a written warning, a final written warning, or dismissal.
|
||||
|
||||
<strong>Stage 4 — Appeal:</strong> The employee may appeal against any disciplinary outcome within five working days of receiving the written decision.
|
||||
|
||||
Gross misconduct, including theft, fraud, violence, and serious breaches of data protection, may result in summary dismissal without notice.</div>
|
||||
|
||||
<div class="section-heading">7.0 Acknowledgement</div>
|
||||
<div class="text-section">I, the undersigned, acknowledge that I have received, read, and understood the contents of this Employee Handbook. I agree to comply with all policies and procedures set out herein.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Thomas Brennan</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 6 January 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">HR Representative</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Claire Whitfield</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 6 January 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,140 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Executive Summary</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin: 8pt 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{DOCUMENT_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<div>
|
||||
<div class="info-label">Prepared By</div>
|
||||
<div class="info-value">{{PREPARED_BY}}</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="info-label">Date</div>
|
||||
<div class="info-value">{{DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="section-heading">Overview</div>
|
||||
<div class="text-section">{{OVERVIEW_TEXT}}</div>
|
||||
|
||||
<!-- Key Findings -->
|
||||
<div class="section-heading">Key Findings</div>
|
||||
<div class="text-section">{{FINDINGS_TEXT}}</div>
|
||||
|
||||
<!-- Recommendations -->
|
||||
<div class="section-heading">Recommendations</div>
|
||||
<div class="text-section">{{RECOMMENDATIONS_TEXT}}</div>
|
||||
|
||||
<!-- Financial Impact -->
|
||||
<div class="section-heading">Financial Impact</div>
|
||||
<div class="text-section">{{FINANCIAL_IMPACT_TEXT}}</div>
|
||||
|
||||
<!-- Next Steps -->
|
||||
<div class="section-heading">Next Steps</div>
|
||||
<div class="text-section">{{NEXT_STEPS_TEXT}}</div>
|
||||
|
||||
<!-- Conclusion -->
|
||||
<div class="section-heading">Conclusion</div>
|
||||
<div class="text-section">{{CONCLUSION_TEXT}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,98 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Executive Summary</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin: 8pt 0; }
|
||||
.info-label { font-weight: 600; font-size: 8.5pt; color: var(--theme-text-muted); }
|
||||
.info-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Executive Summary</div>
|
||||
<div class="doc-subtitle">Aldgate Capital Partners LLP</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div style="font-size: 11pt; font-weight: 600; color: var(--theme-heading); margin: 8pt 0 10pt;">UK Retail Sector Expansion Opportunity</div>
|
||||
<div class="meta-grid">
|
||||
<div>
|
||||
<div class="info-label">Prepared By</div>
|
||||
<div class="info-value">Sophia Hartley, Director of Strategy</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="info-label">Date</div>
|
||||
<div class="info-value">19 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Overview</div>
|
||||
<div class="text-section">This executive summary presents the findings of a three-month strategic review into potential expansion of Aldgate Capital Partners' retail sector portfolio across the Midlands and North of England. The review was commissioned by the Investment Committee in November 2025 and draws on market data, competitor analysis, and direct stakeholder interviews with eight target acquisition companies.
|
||||
|
||||
The review assessed 14 candidate businesses with combined annual revenues of £320 million and identified three high-priority targets that meet the firm's return threshold of 22% IRR over a five-year horizon.</div>
|
||||
<div class="section-heading">Key Findings</div>
|
||||
<div class="text-section">1. The UK independent retail sector has shown resilient 6.2% revenue growth in 2025 despite macroeconomic headwinds, driven by consumer preference for experiential shopping and local provenance brands.
|
||||
|
||||
2. Three acquisition targets — Fenwick North, BrightGrocer Holdings, and Pennine Outdoor — demonstrate strong EBITDA margins of 14–19%, proven management teams, and clear scalability through supply-chain optimisation.
|
||||
|
||||
3. Consolidation activity in the sector has accelerated: seven transactions were completed in the Midlands alone in H2 2025, suggesting a narrowing window for acquisitions at current valuations.
|
||||
|
||||
4. Regulatory risk is low; no material competition or planning obstacles have been identified for the preferred targets.
|
||||
|
||||
5. Digital transformation readiness varies: Fenwick North is category-leading; BrightGrocer and Pennine Outdoor require investment of approximately £2.8 million combined to reach baseline e-commerce capability.</div>
|
||||
<div class="section-heading">Recommendations</div>
|
||||
<div class="text-section">1. Initiate formal due diligence on Fenwick North (primary target) immediately, with a target indicative offer by 15 April 2026. Estimated enterprise value: £48–54 million.
|
||||
|
||||
2. Commission a separate operational review of BrightGrocer Holdings' supply chain to validate management's 23% cost-reduction claim before advancing to Heads of Terms.
|
||||
|
||||
3. Place Pennine Outdoor on a six-month watchlist; monitor Q1 2026 trading results before committing further resource. A deterioration in EBITDA margin below 12% should trigger a withdrawal from consideration.
|
||||
|
||||
4. Engage the firm's preferred legal counsel (Clifford Chance LLP) and financial advisers (Rothschild & Co) by end of February 2026 to prepare transaction infrastructure.</div>
|
||||
<div class="section-heading">Financial Impact</div>
|
||||
<div class="text-section">Acquiring Fenwick North at the midpoint valuation of £51 million (using a 60:40 debt/equity structure) is projected to generate:
|
||||
- IRR: 24.3% over five years (base case)
|
||||
- Cash-on-cash multiple: 2.8x
|
||||
- Projected EBITDA contribution to portfolio in Year 3: £9.2 million
|
||||
|
||||
Sensitivity analysis indicates that even under a downside scenario assuming 10% revenue contraction and 200bps margin compression, the IRR remains above the 18% hurdle rate. The full financial model is available in Appendix B.</div>
|
||||
<div class="section-heading">Next Steps</div>
|
||||
<div class="text-section">1. Investment Committee approval of this recommendation by 28 February 2026.
|
||||
2. Execution of an NDA with Fenwick North management — week of 2 March 2026.
|
||||
3. Kick-off of management presentations and site visits — week of 9 March 2026.
|
||||
4. Indicative offer submission — by 15 April 2026.
|
||||
5. Parallel engagement with BrightGrocer operational review team — March 2026 start.</div>
|
||||
<div class="section-heading">Conclusion</div>
|
||||
<div class="text-section">The strategic review confirms that a disciplined, targeted entry into the UK Midlands and Northern retail sector presents a compelling risk-adjusted opportunity for Aldgate Capital Partners. Fenwick North represents an immediately actionable priority with strong fundamentals and a management team that has expressed openness to a structured transaction. Swift action is recommended to pre-empt competing interest from two identified trade buyers. The Investment Committee's approval to proceed is sought at the next scheduled meeting on 26 February 2026.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,313 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Expense Report</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Employee info ── */
|
||||
.employee-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin: 8pt 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Tables ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 7.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 4pt 5pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num { text-align: right; }
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 3pt 5pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Totals ── */
|
||||
.totals-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.totals-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 9pt;
|
||||
min-width: 140pt;
|
||||
}
|
||||
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
|
||||
.totals-table td:first-child {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.totals-table td:last-child {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.totals-table .total-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
border-top: 1pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-top: 4pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
/* ── Reimbursement note ── */
|
||||
.reimbursement-note {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.totals-wrapper { break-inside: avoid; page-break-inside: avoid; }
|
||||
.totals-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.employee-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Expense Report</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Employee Info -->
|
||||
<div class="employee-grid">
|
||||
<div>
|
||||
<div class="info-label">Employee Name</div>
|
||||
<div class="info-value">{{EMPLOYEE_NAME}}</div>
|
||||
<div class="info-label" style="margin-top:4pt">Department</div>
|
||||
<div class="info-value">{{EMPLOYEE_DEPARTMENT}}</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="info-label">Submission Date</div>
|
||||
<div class="info-value">{{SUBMISSION_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expense Items -->
|
||||
<div class="section-heading">Expense Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18pt">#</th>
|
||||
<th style="width:44pt">Date</th>
|
||||
<th>Description</th>
|
||||
<th style="width:44pt">Category</th>
|
||||
<th style="width:44pt">Merchant</th>
|
||||
<th style="width:28pt">Receipt</th>
|
||||
<th class="num" style="width:22pt">Qty</th>
|
||||
<th class="num" style="width:40pt">Unit Cost</th>
|
||||
<th class="num" style="width:40pt">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{EXPENSE_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Mileage -->
|
||||
<div class="section-heading">Mileage</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18pt">#</th>
|
||||
<th style="width:44pt">Date</th>
|
||||
<th>Purpose</th>
|
||||
<th style="width:50pt">From</th>
|
||||
<th style="width:50pt">To</th>
|
||||
<th class="num" style="width:40pt">Miles</th>
|
||||
<th class="num" style="width:40pt">Rate</th>
|
||||
<th class="num" style="width:40pt">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{MILEAGE_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Summary by Category -->
|
||||
<div class="section-heading">Summary by Category</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th class="num" style="width:60pt">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{SUMMARY_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr class="total-row"><td>Total Amount:</td><td>{{TOTAL_AMOUNT_TEXT}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Reimbursement -->
|
||||
<div class="section-heading">Reimbursement Details</div>
|
||||
<div class="reimbursement-note">{{REIMBURSEMENT_TEXT}}</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Sign-Off</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{EMPLOYEE_SIGNER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{EMPLOYEE_SIGN_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Manager Approval</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{MANAGER_SIGNER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{MANAGER_SIGN_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,225 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Expense Report</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.employee-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin: 8pt 0; }
|
||||
.info-label { font-weight: 600; font-size: 8.5pt; color: var(--theme-text-muted); }
|
||||
.info-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 7.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 4pt 5pt; text-align: left; font-weight: 600; white-space: nowrap; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 3pt 5pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.totals-wrapper { display: flex; justify-content: flex-end; margin-bottom: 8pt; }
|
||||
.totals-table { border-collapse: collapse; font-size: 9pt; min-width: 140pt; }
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
.totals-table td:first-child { color: var(--theme-text-muted); }
|
||||
.totals-table td:last-child { text-align: right; }
|
||||
.totals-table .total-row td { font-weight: 700; font-size: 10pt; border-top: 1pt solid var(--theme-primary); padding-top: 4pt; color: var(--theme-heading); }
|
||||
.reimbursement-note { font-size: 8.5pt; line-height: 1.5; margin-bottom: 4pt; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Expense Report</div>
|
||||
<div class="doc-subtitle">Thornfield Media Group plc</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="employee-grid">
|
||||
<div>
|
||||
<div class="info-label">Employee Name</div>
|
||||
<div class="info-value">Daniel Fitzgerald</div>
|
||||
<div class="info-label" style="margin-top:4pt">Department</div>
|
||||
<div class="info-value">Commercial Sales</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="info-label">Submission Date</div>
|
||||
<div class="info-value">31 January 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Expense Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18pt">#</th>
|
||||
<th style="width:44pt">Date</th>
|
||||
<th>Description</th>
|
||||
<th style="width:44pt">Category</th>
|
||||
<th style="width:44pt">Merchant</th>
|
||||
<th style="width:28pt">Receipt</th>
|
||||
<th class="num" style="width:22pt">Qty</th>
|
||||
<th class="num" style="width:40pt">Unit Cost</th>
|
||||
<th class="num" style="width:40pt">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>14 Jan 2026</td>
|
||||
<td>Return train — London Euston to Manchester Piccadilly</td>
|
||||
<td>Travel</td>
|
||||
<td>Avanti West Coast</td>
|
||||
<td>R-001</td>
|
||||
<td class="num">1</td>
|
||||
<td class="num">£142.00</td>
|
||||
<td class="num">£142.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>14 Jan 2026</td>
|
||||
<td>Client lunch — 3 attendees (Prospect meeting)</td>
|
||||
<td>Meals</td>
|
||||
<td>The Ivy, Manchester</td>
|
||||
<td>R-002</td>
|
||||
<td class="num">1</td>
|
||||
<td class="num">£87.50</td>
|
||||
<td class="num">£87.50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>15 Jan 2026</td>
|
||||
<td>Hotel accommodation — 1 night</td>
|
||||
<td>Accommodation</td>
|
||||
<td>Radisson Blu</td>
|
||||
<td>R-003</td>
|
||||
<td class="num">1</td>
|
||||
<td class="num">£129.00</td>
|
||||
<td class="num">£129.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>22 Jan 2026</td>
|
||||
<td>Taxi to London Heathrow Terminal 5</td>
|
||||
<td>Travel</td>
|
||||
<td>Addison Lee</td>
|
||||
<td>R-004</td>
|
||||
<td class="num">1</td>
|
||||
<td class="num">£54.00</td>
|
||||
<td class="num">£54.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>22 Jan 2026</td>
|
||||
<td>Return flight — London Heathrow to Edinburgh</td>
|
||||
<td>Travel</td>
|
||||
<td>British Airways</td>
|
||||
<td>R-005</td>
|
||||
<td class="num">1</td>
|
||||
<td class="num">£218.00</td>
|
||||
<td class="num">£218.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading">Mileage</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18pt">#</th>
|
||||
<th style="width:44pt">Date</th>
|
||||
<th>Purpose</th>
|
||||
<th style="width:50pt">From</th>
|
||||
<th style="width:50pt">To</th>
|
||||
<th class="num" style="width:40pt">Miles</th>
|
||||
<th class="num" style="width:40pt">Rate</th>
|
||||
<th class="num" style="width:40pt">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>19 Jan 2026</td>
|
||||
<td>Client site visit</td>
|
||||
<td>Home office</td>
|
||||
<td>Basingstoke</td>
|
||||
<td class="num">48</td>
|
||||
<td class="num">£0.45</td>
|
||||
<td class="num">£21.60</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading">Summary by Category</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th class="num" style="width:60pt">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Accommodation</td><td class="num">£129.00</td></tr>
|
||||
<tr><td>Meals</td><td class="num">£87.50</td></tr>
|
||||
<tr><td>Mileage</td><td class="num">£21.60</td></tr>
|
||||
<tr><td>Travel</td><td class="num">£414.00</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr class="total-row"><td>Total Amount:</td><td>£652.10</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Reimbursement Details</div>
|
||||
<div class="reimbursement-note">Please reimburse to Daniel Fitzgerald's designated expenses account. Payment is expected within 10 working days of approval.</div>
|
||||
<div class="section-heading">Sign-Off</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Daniel Fitzgerald</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 31 January 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Manager Approval</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Harriet Dawson</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 3 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,294 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Incident Report</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Incident summary grid ── */
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin-bottom: 8pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.summary-cell {
|
||||
padding: 4pt 7pt;
|
||||
border-right: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-bottom: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.summary-cell:nth-child(even) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 7.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
margin-bottom: 1pt;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
/* ── Severity badge ── */
|
||||
.severity-critical {
|
||||
display: inline-block;
|
||||
background: #fca5a5;
|
||||
color: #7f1d1d;
|
||||
font-weight: 700;
|
||||
font-size: 8pt;
|
||||
padding: 1pt 6pt;
|
||||
border-radius: 2pt;
|
||||
}
|
||||
|
||||
.severity-high {
|
||||
display: inline-block;
|
||||
background: #fed7aa;
|
||||
color: #7c2d12;
|
||||
font-weight: 700;
|
||||
font-size: 8pt;
|
||||
padding: 1pt 6pt;
|
||||
border-radius: 2pt;
|
||||
}
|
||||
|
||||
.severity-medium {
|
||||
display: inline-block;
|
||||
background: #fef08a;
|
||||
color: #713f12;
|
||||
font-weight: 700;
|
||||
font-size: 8pt;
|
||||
padding: 1pt 6pt;
|
||||
border-radius: 2pt;
|
||||
}
|
||||
|
||||
.severity-low {
|
||||
display: inline-block;
|
||||
background: #bbf7d0;
|
||||
color: #14532d;
|
||||
font-weight: 700;
|
||||
font-size: 8pt;
|
||||
padding: 1pt 6pt;
|
||||
border-radius: 2pt;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Sign-off ── */
|
||||
.signoff-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.signoff-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.signoff-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 18pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.signoff-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Incident Report</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}} · Report No: {{REPORT_NUMBER}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Incident Summary Grid -->
|
||||
<div class="summary-grid">
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Incident Date</div>
|
||||
<div class="summary-value">{{INCIDENT_DATE}}</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Incident Time</div>
|
||||
<div class="summary-value">{{INCIDENT_TIME}}</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Location</div>
|
||||
<div class="summary-value">{{INCIDENT_LOCATION}}</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Incident Type</div>
|
||||
<div class="summary-value">{{INCIDENT_TYPE}}</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Severity Level</div>
|
||||
<div class="summary-value">{{SEVERITY_LEVEL}}</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Reporter</div>
|
||||
<div class="summary-value">{{REPORTER_NAME}}, {{REPORTER_TITLE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- People Involved -->
|
||||
<div class="section-heading">People Involved</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reported By:</span> <span class="meta-value">{{REPORTER_NAME}}, {{REPORTER_TITLE}}</span>
|
||||
<span class="meta-label">Witnesses:</span> <span class="meta-value">{{WITNESSES_TEXT}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Incident Description -->
|
||||
<div class="section-heading">Incident Description</div>
|
||||
<div class="text-section">{{DESCRIPTION_TEXT}}</div>
|
||||
|
||||
<!-- Immediate Actions Taken -->
|
||||
<div class="section-heading">Immediate Actions Taken</div>
|
||||
<div class="text-section">{{IMMEDIATE_ACTIONS_TEXT}}</div>
|
||||
|
||||
<!-- Root Cause Analysis -->
|
||||
<div class="section-heading">Root Cause Analysis</div>
|
||||
<div class="text-section">{{ROOT_CAUSE_TEXT}}</div>
|
||||
|
||||
<!-- Corrective Actions -->
|
||||
<div class="section-heading">Corrective Actions</div>
|
||||
<div class="text-section">{{CORRECTIVE_ACTIONS_TEXT}}</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div class="section-heading">Attachments</div>
|
||||
<div class="text-section">{{ATTACHMENTS_TEXT}}</div>
|
||||
|
||||
<!-- Sign-off -->
|
||||
<div class="section-heading">Sign-Off and Review</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Review Date:</span> <span class="meta-value">{{REVIEW_DATE}}</span>
|
||||
</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Reviewer / Authoriser</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{REVIEWER_NAME}}</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Reporter</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{REPORTER_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,158 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Incident Report</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border: 0.5pt solid var(--theme-border); margin-bottom: 8pt; font-size: 8.5pt; }
|
||||
.summary-cell { padding: 4pt 7pt; border-right: 0.5pt solid var(--theme-border); border-bottom: 0.5pt solid var(--theme-border); }
|
||||
.summary-cell:nth-child(even) { border-right: none; }
|
||||
.summary-label { font-weight: 600; color: var(--theme-text-muted); font-size: 7.5pt; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 1pt; }
|
||||
.summary-value { color: var(--theme-text); font-size: 8.5pt; }
|
||||
.severity-high { display: inline-block; background: #fed7aa; color: #7c2d12; font-weight: 700; font-size: 8pt; padding: 1pt 6pt; border-radius: 2pt; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.6; margin-bottom: 4pt; }
|
||||
.signoff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 14pt; }
|
||||
.signoff-col { font-size: 8.5pt; }
|
||||
.signoff-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 18pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Incident Report</div>
|
||||
<div class="doc-subtitle">Ironbridge Engineering Solutions Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Report No</div>
|
||||
<div class="doc-number">IR-2026-041</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Incident Date</div>
|
||||
<div class="summary-value">4 February 2026</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Incident Time</div>
|
||||
<div class="summary-value">08:47</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Location</div>
|
||||
<div class="summary-value">Workshop Bay 3, Telford Manufacturing Site, TF1 5RQ</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Incident Type</div>
|
||||
<div class="summary-value">Workplace Injury – Manual Handling</div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Severity Level</div>
|
||||
<div class="summary-value"><span class="severity-high">High</span></div>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<div class="summary-label">Reporter</div>
|
||||
<div class="summary-value">Gary Lockwood, Shift Supervisor</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">People Involved</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reported By:</span> <span class="meta-value">Gary Lockwood, Shift Supervisor</span>
|
||||
<span class="meta-label">Witnesses:</span> <span class="meta-value">Trevor Baines (Fabrication Technician); Sandra Okafor (Health and Safety Officer)</span>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Incident Description</div>
|
||||
<div class="text-section">At approximately 08:47 on 4 February 2026, Ian Morecroft (Fabrication Operative, Grade 3) sustained an injury to his lower back while manually moving a steel fabrication jig weighing approximately 42 kg in Workshop Bay 3. Mr Morecroft had been attempting to reposition the jig from a floor-level pallet onto a workbench without mechanical lifting assistance. He did not use the overhead gantry hoist fitted in Bay 3, citing that it was in use by a colleague for a separate task.
|
||||
|
||||
Mr Morecroft reported immediate onset of acute lower back pain and was unable to continue work. He was escorted to the site first-aid room by Shift Supervisor Gary Lockwood at 08:52. The on-site first aider assessed the injury and called 999 at 09:10 due to worsening symptoms. Mr Morecroft was transported to Princess Royal Hospital, Telford, by ambulance at 09:25. He was diagnosed with a lumbar muscle strain and minor disc compression and was signed off work for a minimum of four weeks.</div>
|
||||
|
||||
<div class="section-heading">Immediate Actions Taken</div>
|
||||
<div class="text-section">1. Mr Morecroft was removed from Workshop Bay 3 immediately and first aid was administered on-site.
|
||||
2. Emergency services were called at 09:10 and Mr Morecroft was transported to hospital by ambulance.
|
||||
3. Workshop Bay 3 was cordoned off pending investigation and the jig was secured in place.
|
||||
4. The incident was verbally reported to the Site Health and Safety Manager (Sandra Okafor) at 09:00 and formally notified to the Health and Safety Executive (HSE) under RIDDOR 2013 on 4 February 2026 (Reference: HSE-RIDDOR-2026-11874).
|
||||
5. All manual handling activities in Workshop Bay 3 were suspended pending a risk assessment review.
|
||||
6. Mr Morecroft's next of kin were notified at 09:30.</div>
|
||||
|
||||
<div class="section-heading">Root Cause Analysis</div>
|
||||
<div class="text-section">Primary cause: Failure to follow the site Manual Handling Procedure (SHE-P-004, Rev 2) which requires mechanical lifting assistance for loads exceeding 25 kg. Mr Morecroft confirmed he was aware of the procedure but considered the move to be a quick, short-distance task.
|
||||
|
||||
Contributing factors:
|
||||
- The sole overhead gantry hoist in Bay 3 was in use at the time of the incident, creating a perceived operational bottleneck that encouraged unsafe ad hoc lifting.
|
||||
- A toolbox talk on manual handling conducted in November 2025 did not include a practical demonstration or competency check for operatives working with heavy fabrication components.
|
||||
- The risk assessment for Bay 3 workbench loading tasks (RA-BAY3-001) had not been reviewed since March 2024 and did not reflect the addition of heavier jig stock introduced in September 2025.</div>
|
||||
|
||||
<div class="section-heading">Corrective Actions</div>
|
||||
<div class="text-section">1. Install a second overhead gantry hoist in Workshop Bay 3 to eliminate single-point bottlenecks for heavy lifting tasks. Target completion: 28 February 2026. Owner: Operations Manager (D. Hartington).
|
||||
2. Conduct a refresher manual handling training session for all Bay 3 operatives, including a practical competency assessment. Target completion: 14 February 2026. Owner: Health and Safety Officer (S. Okafor).
|
||||
3. Review and update risk assessment RA-BAY3-001 to reflect current load weights and introduce a mandatory pre-lift checklist. Target completion: 11 February 2026. Owner: Health and Safety Officer (S. Okafor).
|
||||
4. Brief all shift supervisors on enforcement of mechanical assistance requirements and the escalation procedure when equipment is unavailable. Target completion: 7 February 2026. Owner: Site Manager (P. Fairweather).
|
||||
5. Review all site risk assessments for manual handling tasks to identify any others that have not been updated within the 12-month review cycle. Target completion: 31 March 2026. Owner: Health and Safety Officer (S. Okafor).</div>
|
||||
|
||||
<div class="section-heading">Attachments</div>
|
||||
<div class="text-section">1. First Aid Record – Bay 3 First Aid Room Log, 4 February 2026
|
||||
2. RIDDOR Notification Confirmation – HSE Reference HSE-RIDDOR-2026-11874
|
||||
3. Risk Assessment RA-BAY3-001 (current version, March 2024)
|
||||
4. Manual Handling Procedure SHE-P-004 Rev 2
|
||||
5. Workshop Bay 3 Site Photographs (8 images, taken 4 February 2026 09:45)
|
||||
6. Witness Statement – Trevor Baines, dated 4 February 2026
|
||||
7. Witness Statement – Sandra Okafor, dated 4 February 2026</div>
|
||||
|
||||
<div class="section-heading">Sign-Off and Review</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Review Date:</span> <span class="meta-value">6 February 2026</span>
|
||||
</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Reviewer / Authoriser</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Paul Fairweather, Site Manager</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Reporter</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Gary Lockwood, Shift Supervisor</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,373 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Independent Contractor Agreement</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 130pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Parties ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Legal sections ── */
|
||||
.legal-section {
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.legal-section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.legal-section-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Sub-section ── */
|
||||
.legal-subsection {
|
||||
margin-top: 6pt;
|
||||
margin-bottom: 6pt;
|
||||
margin-left: 10pt;
|
||||
}
|
||||
|
||||
.legal-subsection-heading {
|
||||
font-size: 9pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-subsection-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-col { break-inside: avoid; page-break-inside: avoid; }
|
||||
.parties-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{AGREEMENT_TITLE}}</div>
|
||||
<div class="doc-subtitle">Independent Contractor Agreement</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">{{AGREEMENT_REF}}</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">{{GOVERNING_LAW}}</span>
|
||||
<span class="meta-label">Jurisdiction:</span> <span class="meta-value">{{JURISDICTION}}</span>
|
||||
<span class="meta-label">Classification:</span> <span class="meta-value">{{CONFIDENTIALITY_LABEL}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Company</div>
|
||||
<div class="party-name">{{COMPANY_LEGAL_NAME}}</div>
|
||||
<div class="party-detail">{{COMPANY_ENTITY_TYPE}}</div>
|
||||
<div class="party-detail">Reg. No.: {{COMPANY_REG_NO}}</div>
|
||||
<div class="party-detail">{{COMPANY_ADDRESS}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Contractor</div>
|
||||
<div class="party-name">{{CONTRACTOR_LEGAL_NAME}}</div>
|
||||
<div class="party-detail">{{CONTRACTOR_ENTITY_TYPE}}</div>
|
||||
<div class="party-detail">Reg. No.: {{CONTRACTOR_REG_NO}}</div>
|
||||
<div class="party-detail">{{CONTRACTOR_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal sections -->
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Services</div>
|
||||
<div class="legal-section-body">{{SERVICES_DESCRIPTION_TEXT}}</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.1 Deliverables</div>
|
||||
<div class="legal-section-body">{{DELIVERABLES_ITEMS}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.2 Term</div>
|
||||
<div class="legal-section-body">{{TERM_TEXT}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.3 Work Location</div>
|
||||
<div class="legal-section-body">{{WORK_LOCATION_TEXT}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.4 Time Commitment</div>
|
||||
<div class="legal-section-body">{{TIME_COMMITMENT_TEXT}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Fees and Expenses</div>
|
||||
<div class="legal-section-body">{{FEES_TEXT}}</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.1 Expenses</div>
|
||||
<div class="legal-section-body">{{EXPENSES_TEXT}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.2 Invoicing</div>
|
||||
<div class="legal-section-body">{{INVOICING_TEXT}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.3 Payment Terms</div>
|
||||
<div class="legal-section-body">{{PAYMENT_TERMS_TEXT}}</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.4 Tax Status</div>
|
||||
<div class="legal-section-body">{{TAX_STATUS_TEXT}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Intellectual Property</div>
|
||||
<div class="legal-section-body">{{IP_ASSIGNMENT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Confidentiality</div>
|
||||
<div class="legal-section-body">{{CONFIDENTIALITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Data Protection</div>
|
||||
<div class="legal-section-body">{{DATA_PROTECTION_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Non-Solicitation</div>
|
||||
<div class="legal-section-body">{{NON_SOLICIT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Non-Compete</div>
|
||||
<div class="legal-section-body">{{NON_COMPETE_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Independent Contractor Status</div>
|
||||
<div class="legal-section-body">{{INDEPENDENT_STATUS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. Warranties</div>
|
||||
<div class="legal-section-body">{{WARRANTIES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">10. Limitation of Liability</div>
|
||||
<div class="legal-section-body">{{LIABILITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">11. Indemnity</div>
|
||||
<div class="legal-section-body">{{INDEMNITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">12. Termination</div>
|
||||
<div class="legal-section-body">{{TERMINATION_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">13. Notices</div>
|
||||
<div class="legal-section-body">{{NOTICES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">14. Assignment</div>
|
||||
<div class="legal-section-body">{{ASSIGNMENT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">15. Entire Agreement</div>
|
||||
<div class="legal-section-body">{{ENTIRE_AGREEMENT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedules -->
|
||||
<div class="section-heading">Schedules</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-body">{{SCHEDULES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">IN WITNESS WHEREOF, the parties have caused this Agreement to be executed by their duly authorised representatives.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Company</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: {{COMPANY_SIGNATORY}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{SIGNATURE_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Contractor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: {{CONTRACTOR_SIGNATORY}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{SIGNATURE_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,234 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Independent Contractor Agreement — Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { text-align: center; margin-bottom: 8pt; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 130pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 3pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.legal-section { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.legal-section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.legal-section-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.legal-subsection { margin-top: 6pt; margin-bottom: 6pt; margin-left: 10pt; }
|
||||
.legal-subsection-heading { font-size: 9pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .legal-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<div class="doc-header">
|
||||
<div class="doc-title">Independent Contractor Agreement</div>
|
||||
<div class="doc-subtitle">Independent Contractor Agreement</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">ICA-2026-004</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">1 March 2026</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">England and Wales</span>
|
||||
<span class="meta-label">Jurisdiction:</span> <span class="meta-value">Courts of England and Wales</span>
|
||||
<span class="meta-label">Classification:</span> <span class="meta-value">Confidential</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Company</div>
|
||||
<div class="party-name">Vantage Digital Solutions Ltd</div>
|
||||
<div class="party-detail">A private limited company</div>
|
||||
<div class="party-detail">Reg. No.: 11223344</div>
|
||||
<div class="party-detail">25 Broadgate, London, EC2M 2QS, United Kingdom</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Contractor</div>
|
||||
<div class="party-name">Owen Clarke Consulting Ltd</div>
|
||||
<div class="party-detail">A private limited company</div>
|
||||
<div class="party-detail">Reg. No.: 99887766</div>
|
||||
<div class="party-detail">14 Canal Street, Manchester, M1 3HW, United Kingdom</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Services</div>
|
||||
<div class="legal-section-body">The Contractor will provide software engineering and technical architecture services to the Company on a project basis as described in this Agreement and any Statements of Work issued hereunder.</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.1 Deliverables</div>
|
||||
<div class="legal-section-body">Design and implement backend API services for the Company's core product platform.
|
||||
Participate in sprint planning, code reviews, and architecture discussions.
|
||||
Provide written documentation for all major components delivered.
|
||||
Attend weekly project standups (remote attendance acceptable).</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.2 Term</div>
|
||||
<div class="legal-section-body">This Agreement commences on 1 March 2026 and continues for six (6) months, unless earlier terminated. The parties may extend by written agreement.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.3 Work Location</div>
|
||||
<div class="legal-section-body">Services shall be performed primarily remotely. On-site attendance at the Company's London office may be requested with reasonable notice, not to exceed two days per month.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">1.4 Time Commitment</div>
|
||||
<div class="legal-section-body">Approximately three (3) days per week, subject to variation by mutual agreement.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Fees and Expenses</div>
|
||||
<div class="legal-section-body">The Company shall pay the Contractor a daily rate of £650 (exclusive of VAT) for each day of services rendered, as confirmed in approved timesheets.</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.1 Expenses</div>
|
||||
<div class="legal-section-body">Reasonable pre-approved travel and out-of-pocket expenses will be reimbursed at cost on provision of receipts.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.2 Invoicing</div>
|
||||
<div class="legal-section-body">The Contractor shall submit invoices on a monthly basis, itemising days worked and any approved expenses.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.3 Payment Terms</div>
|
||||
<div class="legal-section-body">Payment shall be due within thirty (30) days of the date of a valid invoice.</div>
|
||||
</div>
|
||||
<div class="legal-subsection">
|
||||
<div class="legal-subsection-heading">2.4 Tax Status</div>
|
||||
<div class="legal-section-body">The Contractor is responsible for all taxes, national insurance, and statutory obligations in respect of fees received under this Agreement. The Company shall have no obligation to deduct or withhold taxes unless required by law.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Intellectual Property</div>
|
||||
<div class="legal-section-body">All work product, code, designs, and deliverables created by the Contractor specifically for the Company in the performance of the Services ("Work Product") shall be assigned to and vest in the Company upon creation. The Contractor retains rights in any pre-existing materials and third-party tools incorporated into deliverables, subject to granting the Company a perpetual licence to use such materials for the purposes of the Work Product.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Confidentiality</div>
|
||||
<div class="legal-section-body">The Contractor shall treat all non-public information of the Company as strictly confidential and shall not disclose it to third parties or use it for any purpose other than performing the Services. This obligation shall survive termination for three (3) years.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Data Protection</div>
|
||||
<div class="legal-section-body">The Contractor shall comply with all applicable data protection legislation (including the UK GDPR and the Data Protection Act 2018) in processing any personal data in connection with this Agreement and shall enter into any data processing agreement reasonably required by the Company.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Non-Solicitation</div>
|
||||
<div class="legal-section-body">During the term and for six (6) months after termination, the Contractor shall not directly solicit any employee or contractor of the Company with whom it had material contact, except through general non-targeted advertisements.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Non-Compete</div>
|
||||
<div class="legal-section-body">During the term of this Agreement, the Contractor shall not provide substantially similar services to any direct competitor of the Company without the Company's prior written consent. This restriction shall not apply post-termination.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Independent Contractor Status</div>
|
||||
<div class="legal-section-body">The Contractor is an independent contractor and not an employee, worker, agent, or partner of the Company. The Contractor has no authority to bind the Company and shall not represent itself as doing so.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. Warranties</div>
|
||||
<div class="legal-section-body">The Contractor warrants that it has the right to enter into this Agreement, that the Services will be performed with reasonable skill and care, and that the deliverables will not infringe the intellectual property rights of any third party.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">10. Limitation of Liability</div>
|
||||
<div class="legal-section-body">Neither party shall be liable for indirect or consequential loss. Each party's aggregate liability under this Agreement shall not exceed the total fees paid or payable in the three (3) months preceding the relevant claim, except in respect of death, personal injury, or fraud.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">11. Indemnity</div>
|
||||
<div class="legal-section-body">Each party shall indemnify the other against third-party claims arising from its breach of this Agreement, gross negligence, or wilful misconduct, subject to the limitation of liability above.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">12. Termination</div>
|
||||
<div class="legal-section-body">Either party may terminate this Agreement on thirty (30) days' written notice. Either party may terminate immediately on written notice if the other party commits a material breach that is not remedied within fourteen (14) days of notice, becomes insolvent, or enters administration.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">13. Notices</div>
|
||||
<div class="legal-section-body">Notices shall be in writing and sent by email (with read receipt) or recorded post to the addresses specified in this Agreement.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">14. Assignment</div>
|
||||
<div class="legal-section-body">Neither party may assign or sub-contract its rights or obligations without the prior written consent of the other party, such consent not to be unreasonably withheld.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">15. Entire Agreement</div>
|
||||
<div class="legal-section-body">This Agreement constitutes the entire agreement between the parties and supersedes all prior agreements, representations, and understandings relating to its subject matter.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Schedules</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-body">Schedule 1 — Statement of Work (SOW) Template
|
||||
Schedule 2 — Approved Rates and Expense Policy</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">IN WITNESS WHEREOF, the parties have caused this Agreement to be executed by their duly authorised representatives.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Company</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: Sarah Whitfield, CEO</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 1 March 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Contractor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: Owen Clarke, Director</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 1 March 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,297 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice {{INVOICE_NUMBER}}</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Top accent bar ── */
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta info grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
.meta-value.due-date {
|
||||
font-weight: 600;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Party cards (matching purchase_order style) ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-card {
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-radius: 3pt;
|
||||
padding: 6pt 8pt;
|
||||
}
|
||||
|
||||
.party-card-header {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Line items table ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num { text-align: right; }
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table .desc-col { width: 40%; }
|
||||
|
||||
/* ── Totals block ── */
|
||||
.totals-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.totals-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
min-width: 150pt;
|
||||
}
|
||||
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
|
||||
.totals-table td:first-child {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.totals-table td:last-child {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.totals-table .total-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
border-top: 1pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-top: 4pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.totals-table .balance-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.totals-wrapper { break-inside: avoid; page-break-inside: avoid; }
|
||||
.totals-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.party-card { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Invoice</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Invoice Number</div>
|
||||
<div class="doc-number">{{INVOICE_NUMBER}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Invoice Date:</span> <span class="meta-value">{{INVOICE_DATE}}</span>
|
||||
<span class="meta-label">Due Date:</span> <span class="meta-value due-date">{{DUE_DATE}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">From</div>
|
||||
<div class="party-name">{{SUPPLIER_NAME}}</div>
|
||||
<div class="party-detail">{{SUPPLIER_ADDRESS}}</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Bill To</div>
|
||||
<div class="party-name">{{CUSTOMER_NAME}}</div>
|
||||
<div class="party-detail">{{CUSTOMER_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="section-heading">Line Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22pt">#</th>
|
||||
<th class="desc-col">Description</th>
|
||||
<th class="num" style="width:32pt">Qty</th>
|
||||
<th style="width:32pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{LINEITEM_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>{{SUBTOTAL_TEXT}}</td></tr>
|
||||
<tr><td>Tax:</td><td>{{TAX_TEXT}}</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>{{TOTAL_TEXT}}</td></tr>
|
||||
<tr class="balance-row"><td>Balance Due:</td><td>{{BALANCE_DUE_TEXT}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Payment Terms -->
|
||||
<div class="section-heading">Payment Terms</div>
|
||||
<div class="text-section">{{PAYMENT_TERMS_TEXT}}</div>
|
||||
|
||||
<!-- Bank Details -->
|
||||
<div class="section-heading">Bank Details</div>
|
||||
<div class="text-section">{{BANK_DETAILS_TEXT}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,155 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.meta-value.due-date { font-weight: 600; color: var(--theme-accent); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-card { border: 0.5pt solid var(--theme-border); border-radius: 3pt; padding: 6pt 8pt; }
|
||||
.party-card-header { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 4pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; white-space: nowrap; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.totals-wrapper { display: flex; justify-content: flex-end; margin-bottom: 10pt; }
|
||||
.totals-table { border-collapse: collapse; font-size: 8.5pt; min-width: 150pt; }
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
.totals-table td:first-child { color: var(--theme-text-muted); }
|
||||
.totals-table td:last-child { text-align: right; }
|
||||
.totals-table .total-row td { font-weight: 700; font-size: 10pt; border-top: 1pt solid var(--theme-primary); padding-top: 4pt; color: var(--theme-heading); }
|
||||
.totals-table .balance-row td { font-weight: 700; font-size: 10pt; color: var(--theme-accent); }
|
||||
.text-section { font-size: 8.5pt; line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Invoice</div>
|
||||
<div class="doc-subtitle">Hartwell Consulting Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Invoice Number</div>
|
||||
<div class="doc-number">INV-2026-0047</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Invoice Date:</span> <span class="meta-value">15 January 2026</span>
|
||||
<span class="meta-label">Due Date:</span> <span class="meta-value due-date">14 February 2026</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">From</div>
|
||||
<div class="party-name">Hartwell Consulting Ltd</div>
|
||||
<div class="party-detail">22 Canary Wharf, London, E14 5AB, UK</div>
|
||||
<div class="party-detail">VAT No: GB123456789</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Bill To</div>
|
||||
<div class="party-name">Pemberton Industries plc</div>
|
||||
<div class="party-detail">8 Bridge Street, Manchester, M1 2JF, UK</div>
|
||||
<div class="party-detail">Accounts Payable Dept.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Line Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22pt">#</th>
|
||||
<th style="width:40%">Description</th>
|
||||
<th class="num" style="width:32pt">Qty</th>
|
||||
<th style="width:32pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Strategic advisory — Q4 2025 retainer</td>
|
||||
<td class="num">1</td>
|
||||
<td>month</td>
|
||||
<td class="num">£4,500.00</td>
|
||||
<td class="num">£4,500.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Market analysis report — Sector Overview 2026</td>
|
||||
<td class="num">1</td>
|
||||
<td>report</td>
|
||||
<td class="num">£1,200.00</td>
|
||||
<td class="num">£1,200.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Workshop facilitation (half-day, 12 Jan 2026)</td>
|
||||
<td class="num">1</td>
|
||||
<td>session</td>
|
||||
<td class="num">£950.00</td>
|
||||
<td class="num">£950.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>£6,650.00</td></tr>
|
||||
<tr><td>VAT (20%):</td><td>£1,330.00</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>£7,980.00</td></tr>
|
||||
<tr class="balance-row"><td>Balance Due:</td><td>£7,980.00</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Payment Terms</div>
|
||||
<div class="text-section">Payment is due within 30 days of the invoice date. Late payments may incur interest at 8% per annum above the Bank of England base rate under the Late Payment of Commercial Debts (Interest) Act 1998.</div>
|
||||
<div class="section-heading">Bank Details</div>
|
||||
<div class="text-section">Account Name: Hartwell Consulting Ltd
|
||||
Sort Code: 20-00-00
|
||||
Account Number: 12345678
|
||||
Bank: Lloyds Bank plc, London
|
||||
Reference: INV-2026-0047</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,158 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Job Description</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Bullet lists (li items provided via placeholder) ── */
|
||||
.bullet-list {
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
padding-left: 14pt;
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.bullet-list li {
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.bullet-list li { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{JOB_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Job details -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Department:</span> <span class="meta-value">{{DEPARTMENT}}</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">{{LOCATION}}</span>
|
||||
<span class="meta-label">Employment Type:</span> <span class="meta-value">{{EMPLOYMENT_TYPE}}</span>
|
||||
<span class="meta-label">Salary Range:</span> <span class="meta-value">{{SALARY_RANGE}}</span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- About the Role -->
|
||||
<div class="section-heading">About the Role</div>
|
||||
<div class="text-section">{{ABOUT_ROLE_TEXT}}</div>
|
||||
|
||||
<!-- Key Responsibilities -->
|
||||
<div class="section-heading">Key Responsibilities</div>
|
||||
<ul class="bullet-list">
|
||||
{{RESPONSIBILITIES_ITEMS}}
|
||||
</ul>
|
||||
|
||||
<!-- Required Qualifications -->
|
||||
<div class="section-heading">Required Qualifications</div>
|
||||
<ul class="bullet-list">
|
||||
{{REQUIRED_QUALS_ITEMS}}
|
||||
</ul>
|
||||
|
||||
<!-- Preferred Qualifications -->
|
||||
<div class="section-heading">Preferred Qualifications</div>
|
||||
<ul class="bullet-list">
|
||||
{{PREFERRED_QUALS_ITEMS}}
|
||||
</ul>
|
||||
|
||||
<!-- What We Offer -->
|
||||
<div class="section-heading">What We Offer</div>
|
||||
<ul class="bullet-list">
|
||||
{{BENEFITS_ITEMS}}
|
||||
</ul>
|
||||
|
||||
<!-- How to Apply -->
|
||||
<div class="section-heading">How to Apply</div>
|
||||
<div class="text-section">{{HOW_TO_APPLY_TEXT}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,115 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Job Description</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.job-meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3pt 14pt; font-size: 8.5pt; margin-bottom: 8pt; }
|
||||
.job-meta-item strong { font-weight: 600; color: var(--theme-text-muted); margin-right: 3pt; }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 9.5pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 4pt; white-space: pre-line; }
|
||||
.bullet-list { font-size: 9.5pt; line-height: 1.6; color: var(--theme-text); padding-left: 14pt; margin-bottom: 4pt; }
|
||||
.bullet-list li { margin-bottom: 3pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Job Description</div>
|
||||
<div class="doc-subtitle">Hartwell Digital Agency Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div style="font-size: 14pt; font-weight: 700; color: var(--theme-heading); margin: 6pt 0 4pt;">Lead Software Engineer</div>
|
||||
<div class="job-meta-grid">
|
||||
<div class="job-meta-item"><strong>Department:</strong> Product Engineering</div>
|
||||
<div class="job-meta-item"><strong>Location:</strong> Manchester (Hybrid — 2 days in office)</div>
|
||||
<div class="job-meta-item"><strong>Employment Type:</strong> Full-Time, Permanent</div>
|
||||
<div class="job-meta-item"><strong>Salary Range:</strong> £75,000 – £90,000 per annum</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">About the Role</div>
|
||||
<div class="text-section">Hartwell Digital Agency is looking for an experienced and technically driven Lead Software Engineer to join our Product Engineering team in Manchester. In this role you will own the technical direction of one of our core SaaS product lines, leading a team of six engineers through the full software development lifecycle.
|
||||
|
||||
You will work closely with the Product Manager, Head of Engineering, and key stakeholders to translate strategic objectives into well-architected, scalable, and maintainable software. This is a hands-on leadership role; we expect you to write code, review pull requests, and set the technical standards for the team.</div>
|
||||
|
||||
<div class="section-heading">Key Responsibilities</div>
|
||||
<ul class="bullet-list">
|
||||
<li>Lead the design and delivery of new product features, ensuring high code quality, performance, and security.</li>
|
||||
<li>Manage, mentor, and develop a team of six software engineers, conducting regular one-to-ones and performance reviews.</li>
|
||||
<li>Collaborate with the Product Manager to refine requirements, estimate effort, and prioritise the backlog.</li>
|
||||
<li>Own the technical architecture of assigned product areas; produce and review design documentation.</li>
|
||||
<li>Establish and enforce engineering best practices including code review standards, testing strategies, and deployment processes.</li>
|
||||
<li>Contribute to cross-team initiatives such as platform reliability, developer tooling, and security improvements.</li>
|
||||
<li>Participate in an on-call rota and act as the technical escalation point for production incidents.</li>
|
||||
<li>Stay current with emerging technologies and proactively identify opportunities to improve the product.</li>
|
||||
</ul>
|
||||
|
||||
<div class="section-heading">Required Qualifications</div>
|
||||
<ul class="bullet-list">
|
||||
<li>Minimum 6 years of professional software engineering experience, with at least 2 years in a technical lead or senior role.</li>
|
||||
<li>Strong proficiency in TypeScript and Node.js; experience with React or similar modern front-end frameworks.</li>
|
||||
<li>Solid understanding of RESTful API design, microservices architecture, and cloud-native development (AWS preferred).</li>
|
||||
<li>Experience with relational and NoSQL databases (PostgreSQL and MongoDB or equivalent).</li>
|
||||
<li>Demonstrable experience leading Agile / Scrum teams and facilitating sprint ceremonies.</li>
|
||||
<li>Strong written and verbal communication skills; ability to present technical concepts to non-technical stakeholders.</li>
|
||||
<li>A degree in Computer Science, Software Engineering, or a related discipline, or equivalent practical experience.</li>
|
||||
</ul>
|
||||
|
||||
<div class="section-heading">Preferred Qualifications</div>
|
||||
<ul class="bullet-list">
|
||||
<li>Experience with Kubernetes and containerised deployments (Docker, Helm).</li>
|
||||
<li>Familiarity with event-driven architectures (Kafka, RabbitMQ, or equivalent).</li>
|
||||
<li>AWS certifications (Solutions Architect or Developer – Associate level or above).</li>
|
||||
<li>Prior experience in a digital agency or SaaS product company.</li>
|
||||
<li>Open-source contributions or personal projects demonstrating ongoing technical curiosity.</li>
|
||||
</ul>
|
||||
|
||||
<div class="section-heading">What We Offer</div>
|
||||
<ul class="bullet-list">
|
||||
<li>Competitive salary of £75,000 – £90,000, reviewed annually in April.</li>
|
||||
<li>Annual performance bonus of up to 12% of base salary.</li>
|
||||
<li>25 days' annual leave plus UK bank holidays, rising to 28 days after three years.</li>
|
||||
<li>Employer pension contribution of 6% into a NEST workplace scheme.</li>
|
||||
<li>Private health insurance (Vitality Health) from day one, with option to add family members.</li>
|
||||
<li>£2,000 per year personal learning and development budget.</li>
|
||||
<li>MacBook Pro and peripherals budget of £500 for home office set-up.</li>
|
||||
<li>Cycle-to-work scheme and season ticket loan.</li>
|
||||
<li>Flexible working hours with core hours of 10:00–16:00.</li>
|
||||
</ul>
|
||||
|
||||
<div class="section-heading">How to Apply</div>
|
||||
<div class="text-section">Please submit your CV and a covering letter (maximum one page) explaining why you are interested in this role and what you would bring to the team. Applications should be sent to careers@hartwelldigital.co.uk with the subject line "Lead Software Engineer — [Your Name]".
|
||||
|
||||
The closing date for applications is 13 March 2026. First-stage interviews will take place remotely via video call during the week commencing 23 March 2026. Shortlisted candidates will be invited to a technical assessment and in-person panel interview at our Manchester office.
|
||||
|
||||
Hartwell Digital Agency is an equal opportunities employer. We welcome applications from candidates of all backgrounds and are committed to creating an inclusive workplace. We are happy to discuss reasonable adjustments throughout the recruitment process.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,228 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Business Letter</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Letterhead ── */
|
||||
.letterhead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 8pt;
|
||||
border-bottom: 1.5pt solid var(--theme-accent, #2563eb);
|
||||
margin-bottom: 14pt;
|
||||
}
|
||||
|
||||
.sender-block {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sender-company {
|
||||
font-size: 14pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.sender-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.sender-contact {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
text-align: right;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Date ── */
|
||||
.letter-date {
|
||||
font-size: 9.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 14pt;
|
||||
}
|
||||
|
||||
/* ── Recipient ── */
|
||||
.recipient-block {
|
||||
margin-bottom: 14pt;
|
||||
}
|
||||
|
||||
.recipient-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.recipient-detail {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Subject line ── */
|
||||
.subject-line {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
margin-bottom: 12pt;
|
||||
padding: 4pt 0;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-bottom: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.subject-label {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
margin-right: 4pt;
|
||||
}
|
||||
|
||||
/* ── Salutation ── */
|
||||
.salutation {
|
||||
font-size: 10pt;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Body ── */
|
||||
.body-text {
|
||||
font-size: 10pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
margin-bottom: 14pt;
|
||||
}
|
||||
|
||||
/* ── Closing ── */
|
||||
.closing {
|
||||
font-size: 10pt;
|
||||
margin-bottom: 30pt;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-block {
|
||||
font-size: 9.5pt;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 1pt;
|
||||
}
|
||||
|
||||
.sig-title {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 1pt;
|
||||
}
|
||||
|
||||
.sig-company {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
/* ── Footer extras ── */
|
||||
.letter-footer {
|
||||
margin-top: 16pt;
|
||||
padding-top: 6pt;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-block { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<!-- Letterhead -->
|
||||
<div class="letterhead">
|
||||
<div class="sender-block">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="sender-company">{{SENDER_COMPANY}}</div>
|
||||
<div class="sender-detail">{{SENDER_ADDRESS}}</div>
|
||||
</div>
|
||||
<div class="sender-contact">{{SENDER_CONTACT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="letter-date">{{LETTER_DATE}}</div>
|
||||
|
||||
<!-- Recipient -->
|
||||
<div class="recipient-block">
|
||||
<div class="recipient-name">{{RECIPIENT_NAME}}</div>
|
||||
<div class="recipient-detail">{{RECIPIENT_TITLE}}</div>
|
||||
<div class="recipient-detail">{{RECIPIENT_COMPANY}}</div>
|
||||
<div class="recipient-detail">{{RECIPIENT_ADDRESS}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Subject -->
|
||||
<div class="subject-line">
|
||||
<span class="subject-label">Re:</span>{{SUBJECT_LINE}}
|
||||
</div>
|
||||
|
||||
<!-- Salutation -->
|
||||
<div class="salutation">{{SALUTATION}}</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="body-text">{{BODY_TEXT}}</div>
|
||||
|
||||
<!-- Closing -->
|
||||
<div class="closing">{{CLOSING}}</div>
|
||||
|
||||
<!-- Signature -->
|
||||
<div class="sig-block">
|
||||
<div class="sig-name">{{SENDER_NAME}}</div>
|
||||
<div class="sig-title">{{SENDER_TITLE}}</div>
|
||||
<div class="sig-company">{{SENDER_COMPANY}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: enclosures / cc -->
|
||||
<div class="letter-footer">
|
||||
<div>Enc: {{ENCLOSURES}}</div>
|
||||
<div>cc: {{CC_LIST}}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,294 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Letter of Intent</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 60pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Parties ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Legal sections ── */
|
||||
.legal-section {
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.legal-section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.legal-section-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Proposed terms key-value ── */
|
||||
.terms-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 80pt auto;
|
||||
row-gap: 4pt;
|
||||
margin-top: 4pt;
|
||||
}
|
||||
|
||||
.terms-label {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.terms-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-col { break-inside: avoid; page-break-inside: avoid; }
|
||||
.parties-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.terms-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{DOC_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}}</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Reference</div>
|
||||
<div class="doc-number">{{LOI_REF}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Date / parties meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{LOI_DATE}}</span>
|
||||
<span class="meta-label">From:</span> <span class="meta-value">{{PARTY_A_NAME}}</span>
|
||||
<span class="meta-label">To:</span> <span class="meta-value">{{PARTY_B_NAME}}</span>
|
||||
<span class="meta-label">Confidentiality:</span> <span class="meta-value">{{CONFIDENTIALITY_LABEL}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties detail -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Party A</div>
|
||||
<div class="party-name">{{PARTY_A_NAME}}</div>
|
||||
<div class="party-detail">{{PARTY_A_ADDRESS}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Party B</div>
|
||||
<div class="party-name">{{PARTY_B_NAME}}</div>
|
||||
<div class="party-detail">{{PARTY_B_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal sections -->
|
||||
<div class="section-heading">Terms</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Purpose</div>
|
||||
<div class="legal-section-body">{{PURPOSE_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Proposed Terms (Summary)</div>
|
||||
<div class="terms-grid">
|
||||
<span class="terms-label">Scope:</span> <span class="terms-value">{{PROPOSED_SCOPE_TEXT}}</span>
|
||||
<span class="terms-label">Commercials:</span> <span class="terms-value">{{COMMERCIAL_TERMS_TEXT}}</span>
|
||||
<span class="terms-label">Timeline:</span> <span class="terms-value">{{TIMELINE_TEXT}}</span>
|
||||
<span class="terms-label">Validity:</span> <span class="terms-value">{{VALIDITY_TEXT}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Non-Binding Status</div>
|
||||
<div class="legal-section-body">{{NON_BINDING_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Binding Provisions (If Applicable)</div>
|
||||
<div class="legal-section-body">{{BINDING_PROVISIONS_ITEMS}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">{{SIGNATURE_BLOCK_NOTE}}</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Party A</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{PARTY_A_SIGNER}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{PARTY_A_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Party B</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{PARTY_B_SIGNER}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{PARTY_B_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,154 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Letter of Intent — Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 60pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 3pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.legal-section { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.legal-section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.legal-section-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.terms-grid { display: grid; grid-template-columns: 80pt auto; row-gap: 4pt; margin-top: 4pt; }
|
||||
.terms-label { font-size: 8.5pt; font-weight: 600; color: var(--theme-text-muted); }
|
||||
.terms-value { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .legal-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Letter of Intent</div>
|
||||
<div class="doc-subtitle">Meridian Capital Advisors LLP</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Reference</div>
|
||||
<div class="doc-number">LOI-2026-0011</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Confidentiality:</span> <span class="meta-value">CONFIDENTIAL</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">12 February 2026</span>
|
||||
<span class="meta-label">From:</span> <span class="meta-value">Meridian Capital Advisors LLP</span>
|
||||
<span class="meta-label">To:</span> <span class="meta-value">Trident Manufacturing Group Ltd</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Party A</div>
|
||||
<div class="party-name">Meridian Capital Advisors LLP</div>
|
||||
<div class="party-detail">45 Moorgate, London, EC2R 6AQ, United Kingdom</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Party B</div>
|
||||
<div class="party-name">Trident Manufacturing Group Ltd</div>
|
||||
<div class="party-detail">Unit 12, Phoenix Industrial Estate, Sheffield, S9 2GR, United Kingdom</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Terms</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Purpose</div>
|
||||
<div class="legal-section-body">This Letter of Intent ("LOI") sets out the preliminary understanding between Party A and Party B regarding a proposed strategic investment by Party A in Party B, with a view to supporting Party B's expansion into European markets. This LOI is intended to outline the principal terms of the proposed transaction and to facilitate further due diligence and negotiation between the parties.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Proposed Terms (Summary)</div>
|
||||
<div class="terms-grid">
|
||||
<span class="terms-label">Scope:</span> <span class="terms-value">Minority equity investment by Party A in Party B of up to £3,500,000, representing approximately 18% of the fully diluted share capital post-investment.</span>
|
||||
<span class="terms-label">Commercials:</span> <span class="terms-value">Valuation of Party B at £16,000,000 pre-money; investment structured as a primary subscription for new ordinary shares. Board observer right granted to Party A.</span>
|
||||
<span class="terms-label">Timeline:</span> <span class="terms-value">Target completion within 8 weeks of signing this LOI, subject to satisfactory completion of due diligence and negotiation of definitive documentation.</span>
|
||||
<span class="terms-label">Validity:</span> <span class="terms-value">This LOI expires on 14 March 2026 unless extended by written agreement of both parties.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Non-Binding Status</div>
|
||||
<div class="legal-section-body">Except for the provisions expressly stated as binding below, this LOI does not constitute a legally binding agreement and does not create any obligation on either party to proceed with the proposed transaction. Either party may withdraw from negotiations at any time prior to execution of definitive transaction documents, without liability to the other party.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Binding Provisions (If Applicable)</div>
|
||||
<div class="legal-section-body">The following provisions are intended to be legally binding on the parties:
|
||||
- Confidentiality: Each party agrees to keep the existence and terms of this LOI, and all information exchanged in connection with the proposed transaction, strictly confidential for a period of twelve (12) months.
|
||||
- Exclusivity: Party B agrees not to solicit, negotiate, or enter into any agreement regarding a competing transaction for a period of forty-five (45) days from the date of this LOI.
|
||||
- Governing Law: This LOI shall be governed by the laws of England and Wales, and the parties submit to the exclusive jurisdiction of the courts of England and Wales in respect of the binding provisions.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">Agreed and acknowledged by authorised signatories of each party:</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Party A</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Jonathan Ashford</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Managing Partner</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Party B</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Diane Trevelyan</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Chief Executive Officer</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,90 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Business Letter</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.5; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.letterhead { display: flex; justify-content: space-between; align-items: flex-start; padding-bottom: 8pt; border-bottom: 1.5pt solid var(--theme-accent); margin-bottom: 14pt; }
|
||||
.sender-block { flex: 1; }
|
||||
.sender-company { font-size: 14pt; font-weight: 700; color: var(--theme-primary); margin-bottom: 3pt; }
|
||||
.sender-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.sender-contact { font-size: 8pt; color: var(--theme-text-muted); text-align: right; line-height: 1.6; }
|
||||
.letter-date { font-size: 9.5pt; color: var(--theme-text); margin-bottom: 14pt; }
|
||||
.recipient-block { margin-bottom: 14pt; }
|
||||
.recipient-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); }
|
||||
.recipient-detail { font-size: 9pt; color: var(--theme-text); line-height: 1.5; }
|
||||
.subject-line { font-size: 9.5pt; font-weight: 700; color: var(--theme-primary); margin-bottom: 12pt; padding: 4pt 0; border-top: 0.5pt solid var(--theme-border); border-bottom: 0.5pt solid var(--theme-border); }
|
||||
.subject-label { color: var(--theme-text-muted); font-weight: 600; font-size: 8.5pt; margin-right: 4pt; }
|
||||
.salutation { font-size: 10pt; margin-bottom: 10pt; }
|
||||
.body-text { font-size: 10pt; line-height: 1.6; color: var(--theme-text); white-space: pre-line; margin-bottom: 14pt; }
|
||||
.closing { font-size: 10pt; margin-bottom: 30pt; }
|
||||
.sig-block { font-size: 9.5pt; }
|
||||
.sig-name { font-size: 10pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 1pt; }
|
||||
.sig-title { font-size: 9pt; color: var(--theme-text-muted); margin-bottom: 1pt; }
|
||||
.sig-company { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.letter-footer { margin-top: 16pt; padding-top: 6pt; border-top: 0.5pt solid var(--theme-border); font-size: 8pt; color: var(--theme-text-muted); line-height: 1.6; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="letterhead">
|
||||
<div class="sender-block">
|
||||
<div class="sender-company">Greenway Partners LLP</div>
|
||||
<div class="sender-detail">7 Kings Cross Road, London, WC1X 9HB</div>
|
||||
</div>
|
||||
<div class="sender-contact">
|
||||
Tel: +44 20 7946 0123<br>
|
||||
info@greenwaypartners.co.uk<br>
|
||||
www.greenwaypartners.co.uk
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter-date">19 February 2026</div>
|
||||
<div class="recipient-block">
|
||||
<div class="recipient-name">Ms. Sarah Chen</div>
|
||||
<div class="recipient-detail">Procurement Director</div>
|
||||
<div class="recipient-detail">Titan Manufacturing Ltd</div>
|
||||
<div class="recipient-detail">22 Industrial Estate, Sheffield, S1 4AB</div>
|
||||
</div>
|
||||
<div class="subject-line">
|
||||
<span class="subject-label">Re:</span>Proposal for Supply Chain Consulting Services — Project Horizon
|
||||
</div>
|
||||
<div class="salutation">Dear Ms. Chen,</div>
|
||||
<div class="body-text">Thank you for meeting with us on 12 February 2026 to discuss your supply chain optimisation challenges. We greatly appreciated the opportunity to learn about Titan Manufacturing's operations and the pressures you are facing as you scale production to meet growing European demand.
|
||||
|
||||
Following our discussions, we are pleased to submit this formal proposal for a twelve-week supply chain diagnostic and optimisation engagement. Our team will conduct an end-to-end review of your procurement, inventory, and logistics functions, benchmarking them against industry best practice and identifying measurable opportunities for cost reduction and lead-time improvement.
|
||||
|
||||
Based on our initial assessment, we anticipate delivering savings of between 8 and 14 per cent on total supply chain costs, with an expected payback period of under six months. We would mobilise a dedicated team of three senior consultants with direct sector experience in precision manufacturing and European distribution networks.
|
||||
|
||||
We would welcome the opportunity to present our detailed methodology and proposed workplan at your earliest convenience. Please do not hesitate to contact me directly should you require any further information or wish to arrange a follow-up meeting.</div>
|
||||
<div class="closing">Yours sincerely,</div>
|
||||
<div class="sig-block">
|
||||
<div class="sig-name">Jonathan Rowe</div>
|
||||
<div class="sig-title">Partner, Supply Chain Advisory</div>
|
||||
<div class="sig-company">Greenway Partners LLP</div>
|
||||
</div>
|
||||
<div class="letter-footer">
|
||||
<div>Enc: Project Horizon — Scope of Work Summary (2 pp.); Firm Credentials & Case Studies (6 pp.)</div>
|
||||
<div>cc: Marcus Webb, Senior Manager, Greenway Partners LLP</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,286 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Master Services Agreement</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Parties ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Legal sections ── */
|
||||
.legal-section {
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.legal-section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.legal-section-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-col { break-inside: avoid; page-break-inside: avoid; }
|
||||
.meta-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{DOC_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}}</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Reference</div>
|
||||
<div class="doc-number">{{AGREEMENT_REF}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
<span class="meta-label">Provider:</span> <span class="meta-value">{{PROVIDER_NAME}}</span>
|
||||
<span class="meta-label">Provider Address:</span> <span class="meta-value">{{PROVIDER_ADDRESS}}</span>
|
||||
<span class="meta-label">Client:</span> <span class="meta-value">{{CLIENT_NAME}}</span>
|
||||
<span class="meta-label">Client Address:</span> <span class="meta-value">{{CLIENT_ADDRESS}}</span>
|
||||
<span class="meta-label">Confidentiality:</span> <span class="meta-value">{{CONFIDENTIALITY_LABEL}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Background -->
|
||||
<div class="section-heading">Background</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-body">{{BACKGROUND_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal sections -->
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Scope of Services</div>
|
||||
<div class="legal-section-body">{{SCOPE_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Fees and Payment</div>
|
||||
<div class="legal-section-body">{{FEES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Term and Termination</div>
|
||||
<div class="legal-section-body">{{TERM_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Confidentiality</div>
|
||||
<div class="legal-section-body">{{CONFIDENTIALITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Security / Data Handling</div>
|
||||
<div class="legal-section-body">{{SECURITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Intellectual Property</div>
|
||||
<div class="legal-section-body">{{IP_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Limitation of Liability</div>
|
||||
<div class="legal-section-body">{{LIABILITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Indemnification</div>
|
||||
<div class="legal-section-body">{{INDEMNITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. Governing Law</div>
|
||||
<div class="legal-section-body">{{GOVERNING_LAW_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">{{SIGNATURE_NOTE}}</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Provider</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{PROVIDER_SIGNER}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{PROVIDER_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Client</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{CLIENT_SIGNER}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{CLIENT_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,157 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Master Services Agreement — Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.legal-section { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.legal-section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.legal-section-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .legal-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Master Services Agreement</div>
|
||||
<div class="doc-subtitle">Pinnacle Cloud Services Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Reference</div>
|
||||
<div class="doc-number">MSA-2026-0008</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Confidentiality:</span> <span class="meta-value">CONFIDENTIAL</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">1 February 2026</span>
|
||||
<span class="meta-label">Provider:</span> <span class="meta-value">Pinnacle Cloud Services Ltd</span>
|
||||
<span class="meta-label">Provider Address:</span> <span class="meta-value">22 Finsbury Square, London, EC2A 1DX, United Kingdom</span>
|
||||
<span class="meta-label">Client:</span> <span class="meta-value">Caldwell Retail Group plc</span>
|
||||
<span class="meta-label">Client Address:</span> <span class="meta-value">100 Regent Street, London, W1B 5TB, United Kingdom</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Background</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-body">The Provider is a provider of cloud infrastructure, managed services, and professional technology services. The Client wishes to engage the Provider to deliver services in accordance with Statements of Work ("SOWs") entered into under this Agreement from time to time. This Agreement sets out the terms and conditions governing all such engagements.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Scope of Services</div>
|
||||
<div class="legal-section-body">The Provider shall provide the services described in each SOW issued and executed under this Agreement. Each SOW shall form part of and be subject to this Agreement. In the event of conflict between this Agreement and a SOW, this Agreement shall prevail unless the SOW expressly states otherwise. The Provider may engage subcontractors to perform elements of the services, subject to the Client's written consent where the services involve access to personal data or confidential systems.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Fees and Payment</div>
|
||||
<div class="legal-section-body">Fees for services shall be set out in each SOW. Unless otherwise specified: (a) invoices shall be issued monthly in arrears; (b) payment terms are thirty (30) days from invoice date; (c) all amounts are exclusive of VAT; and (d) late payments shall accrue interest at 4% per annum above the Bank of England base rate. The Provider reserves the right to suspend services upon sixty (60) days' written notice of non-payment.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Term and Termination</div>
|
||||
<div class="legal-section-body">This Agreement shall commence on the Effective Date and continue until terminated by either party on ninety (90) days' written notice. Either party may terminate immediately on written notice if the other party: (a) commits a material breach not remedied within thirty (30) days of written notice; (b) becomes insolvent or enters administration or liquidation; or (c) undergoes a change of control without the other party's consent. Termination of this Agreement shall not affect any SOWs then in force unless the terminating party also terminates those SOWs in accordance with their terms.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Confidentiality</div>
|
||||
<div class="legal-section-body">Each party ("Receiving Party") undertakes to keep confidential all information disclosed by the other party ("Disclosing Party") in connection with this Agreement that is marked confidential or that the Receiving Party knows or ought reasonably to know is confidential. The Receiving Party shall not disclose such information to third parties or use it except to perform this Agreement. This obligation shall not apply to information that is or becomes publicly available through no fault of the Receiving Party, or that is required to be disclosed by law or regulation.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Security / Data Handling</div>
|
||||
<div class="legal-section-body">The Provider shall implement and maintain appropriate technical and organisational security measures to protect Client data against unauthorised access, disclosure, loss, or destruction, consistent with industry best practice. Where the Provider processes personal data on behalf of the Client, the parties shall enter into a data processing agreement compliant with the UK GDPR and any applicable data protection legislation.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Intellectual Property</div>
|
||||
<div class="legal-section-body">Each party retains ownership of its pre-existing intellectual property. Subject to full payment of fees, the Provider grants the Client a non-exclusive, non-transferable licence to use any deliverables produced under a SOW solely for the Client's internal business purposes. Unless otherwise agreed in a SOW, the Provider retains ownership of all proprietary tools, methodologies, and background IP used in delivering the services.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Limitation of Liability</div>
|
||||
<div class="legal-section-body">Neither party shall be liable to the other for indirect, special, consequential, or punitive loss, including loss of profits, revenue, data, or goodwill, arising out of or in connection with this Agreement, even if advised of the possibility of such loss. Subject to the preceding sentence, each party's total aggregate liability under or in connection with this Agreement shall not exceed the greater of: (a) the total fees paid by the Client in the twelve (12) months preceding the event giving rise to the claim; or (b) £250,000. Nothing in this Agreement limits liability for death or personal injury caused by negligence, fraud, or any other liability that cannot be limited by law.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Indemnification</div>
|
||||
<div class="legal-section-body">Each party ("Indemnifying Party") shall indemnify, defend, and hold harmless the other party and its officers, directors, and employees against any third-party claims, damages, costs, and expenses (including reasonable legal fees) arising from: (a) the Indemnifying Party's material breach of this Agreement; (b) the Indemnifying Party's gross negligence or wilful misconduct; or (c) in the case of the Provider, any infringement of a third party's intellectual property rights caused by the services or deliverables. Each party's indemnification obligations are subject to the limitation of liability provisions in Section 7.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. Governing Law</div>
|
||||
<div class="legal-section-body">This Agreement shall be governed by and construed in accordance with the laws of England and Wales. The parties irrevocably submit to the exclusive jurisdiction of the courts of England and Wales to resolve any dispute arising under or in connection with this Agreement.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">Agreed by authorised signatories on behalf of each party:</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Provider</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Marcus Webb</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Chief Commercial Officer</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Client</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Rachel Caldwell</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Chief Technology Officer</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date:</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,289 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Meeting Minutes</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Attendees table ── */
|
||||
.attendees-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.attendees-table thead tr {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.attendees-table thead th {
|
||||
padding: 4pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-border, #e2e8f0);
|
||||
font-size: 8pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.attendees-table tbody td {
|
||||
padding: 3pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
}
|
||||
|
||||
/* ── Discussion items ── */
|
||||
.discussion-item {
|
||||
margin-bottom: 8pt;
|
||||
padding-left: 8pt;
|
||||
border-left: 2pt solid var(--theme-accent, #2563eb);
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.discussion-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.discussion-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Action items table ── */
|
||||
.action-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 8pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.action-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.action-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.status-open { color: #b45309; font-weight: 600; }
|
||||
.status-done { color: #15803d; font-weight: 600; }
|
||||
.status-review { color: #1d4ed8; font-weight: 600; }
|
||||
|
||||
/* ── Sign-off ── */
|
||||
.signoff-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.signoff-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.signoff-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 18pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.discussion-item { break-inside: avoid; page-break-inside: avoid; }
|
||||
.signoff-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
.discussion-item { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{MEETING_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}} · Meeting Minutes</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{MEETING_DATE}}</span>
|
||||
<span class="meta-label">Time:</span> <span class="meta-value">{{MEETING_TIME}}</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">{{MEETING_LOCATION}}</span>
|
||||
<span class="meta-label">Chairperson:</span><span class="meta-value">{{CHAIR_NAME}}</span>
|
||||
<span class="meta-label">Minutes By:</span><span class="meta-value">{{SECRETARY_NAME}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Attendees -->
|
||||
<div class="section-heading">Attendees</div>
|
||||
<table class="attendees-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:35%">Name</th>
|
||||
<th style="width:30%">Title / Role</th>
|
||||
<th style="width:20%">Department</th>
|
||||
<th style="width:15%">Present</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ATTENDEES_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Discussion -->
|
||||
<div class="section-heading">Discussion</div>
|
||||
{{DISCUSSION_ITEMS}}
|
||||
|
||||
<!-- Action Items -->
|
||||
<div class="section-heading">Action Items</div>
|
||||
<table class="action-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%">Action Item</th>
|
||||
<th style="width:20%">Owner</th>
|
||||
<th style="width:20%">Due Date</th>
|
||||
<th style="width:20%">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ACTION_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Next Meeting -->
|
||||
<div class="section-heading">Next Meeting</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date & Time:</span> <span class="meta-value">{{NEXT_MEETING_DATE}}</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">{{NEXT_MEETING_LOCATION}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Sign-off -->
|
||||
<div class="section-heading">Approval</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Prepared By</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{MINUTES_PREPARED_BY}}</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Approved By</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">{{CHAIR_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Meeting Minutes</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.attendees-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.attendees-table thead tr { background: var(--theme-surface); }
|
||||
.attendees-table thead th { padding: 4pt 6pt; text-align: left; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-border); font-size: 8pt; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.attendees-table tbody td { padding: 3pt 6pt; border-bottom: 0.3pt solid var(--theme-border); }
|
||||
.discussion-item { margin-bottom: 8pt; padding-left: 8pt; border-left: 2pt solid var(--theme-accent); page-break-inside: avoid; }
|
||||
.discussion-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.discussion-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.action-table { width: 100%; border-collapse: collapse; margin-bottom: 8pt; font-size: 8.5pt; }
|
||||
.action-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.action-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; }
|
||||
.action-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.action-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.status-open { color: #b45309; font-weight: 600; }
|
||||
.status-done { color: #15803d; font-weight: 600; }
|
||||
.signoff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 14pt; }
|
||||
.signoff-col { font-size: 8.5pt; }
|
||||
.signoff-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 18pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .discussion-item { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Meeting Minutes</div>
|
||||
<div class="doc-subtitle">Stirling Technologies Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">18 February 2026</span>
|
||||
<span class="meta-label">Time:</span> <span class="meta-value">10:00 – 11:45</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">Conference Room B, London HQ</span>
|
||||
<span class="meta-label">Chairperson:</span><span class="meta-value">James Whitfield</span>
|
||||
<span class="meta-label">Minutes By:</span> <span class="meta-value">Amelia Frost</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Attendees</div>
|
||||
<table class="attendees-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:35%">Name</th>
|
||||
<th style="width:30%">Title / Role</th>
|
||||
<th style="width:20%">Department</th>
|
||||
<th style="width:15%">Present</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>James Whitfield</td><td>Chief Technology Officer</td><td>Engineering</td><td>Yes</td></tr>
|
||||
<tr><td>Amelia Frost</td><td>Product Manager</td><td>Product</td><td>Yes</td></tr>
|
||||
<tr><td>Rajan Mehta</td><td>Lead Engineer</td><td>Engineering</td><td>Yes</td></tr>
|
||||
<tr><td>Sophie Langton</td><td>UX Designer</td><td>Design</td><td>Yes</td></tr>
|
||||
<tr><td>Oliver Drummond</td><td>Head of Sales</td><td>Commercial</td><td>Yes</td></tr>
|
||||
<tr><td>Niamh O'Brien</td><td>Data Analyst</td><td>Analytics</td><td>No (apologies sent)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading">Discussion</div>
|
||||
<div class="discussion-item">
|
||||
<div class="discussion-heading">1. Q1 Roadmap Review</div>
|
||||
<div class="discussion-body">James opened with an overview of Q4 2025 delivery performance. Eight of nine planned features shipped on schedule; the smart-folder batch processing feature slipped by two weeks due to third-party API instability. The team agreed to carry this forward as a priority for Q1. Oliver noted positive customer feedback on the PDF merge redesign and requested it be highlighted in upcoming marketing materials.</div>
|
||||
</div>
|
||||
<div class="discussion-item">
|
||||
<div class="discussion-heading">2. New Feature Prioritisation</div>
|
||||
<div class="discussion-body">Amelia presented the backlog ranked by customer impact score. The top three items approved for Q1 development are: (1) AI-assisted document generation templates, (2) bulk processing queue with progress tracking, and (3) white-label branding options for enterprise clients. Sophie noted the design system update must be completed before white-label work begins and estimated a two-week lead time.</div>
|
||||
</div>
|
||||
<div class="discussion-item">
|
||||
<div class="discussion-heading">3. Infrastructure and Performance</div>
|
||||
<div class="discussion-body">Rajan presented benchmark results showing 23% improvement in PDF rendering speed following the January infrastructure upgrade. He proposed migrating the job queue to a dedicated worker cluster to support bulk processing. Estimated cost is £1,200/month; James approved provisionally pending finance sign-off. Target completion date: 14 March 2026.</div>
|
||||
</div>
|
||||
<div class="discussion-item">
|
||||
<div class="discussion-heading">4. Q2 Planning Preview</div>
|
||||
<div class="discussion-body">Brief discussion of Q2 themes: mobile application development and enhanced analytics dashboard. Oliver emphasised that the sales team urgently requires improved usage reporting for enterprise renewal conversations. Amelia to circulate a draft Q2 roadmap for async review by 25 February 2026.</div>
|
||||
</div>
|
||||
<div class="section-heading">Action Items</div>
|
||||
<table class="action-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:42%">Action Item</th>
|
||||
<th style="width:20%">Owner</th>
|
||||
<th style="width:20%">Due Date</th>
|
||||
<th style="width:18%">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Finalise smart-folder batch processing spec and begin sprint</td><td>Rajan Mehta</td><td>25 Feb 2026</td><td class="status-open">Open</td></tr>
|
||||
<tr><td>Complete design system update to unblock white-label work</td><td>Sophie Langton</td><td>4 Mar 2026</td><td class="status-open">Open</td></tr>
|
||||
<tr><td>Submit infrastructure upgrade cost for finance approval</td><td>James Whitfield</td><td>21 Feb 2026</td><td class="status-open">Open</td></tr>
|
||||
<tr><td>Circulate draft Q2 roadmap for team review</td><td>Amelia Frost</td><td>25 Feb 2026</td><td class="status-open">Open</td></tr>
|
||||
<tr><td>Prepare PDF merge feature highlights for marketing</td><td>Oliver Drummond</td><td>28 Feb 2026</td><td class="status-open">Open</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading">Next Meeting</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Date & Time:</span> <span class="meta-value">18 March 2026, 10:00</span>
|
||||
<span class="meta-label">Location:</span> <span class="meta-value">Conference Room B, London HQ</span>
|
||||
</div>
|
||||
<div class="section-heading">Approval</div>
|
||||
<div class="signoff-grid">
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Prepared By</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Amelia Frost</div>
|
||||
</div>
|
||||
<div class="signoff-col">
|
||||
<div class="signoff-label">Approved By</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">James Whitfield</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,311 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Non-Disclosure Agreement</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Party section (plain two-column, no card/border) ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Legal sections ── */
|
||||
.legal-section {
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.legal-section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.legal-section-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-col { break-inside: avoid; page-break-inside: avoid; }
|
||||
.parties-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{AGREEMENT_TITLE}}</div>
|
||||
<div class="doc-subtitle">Non-Disclosure Agreement</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">{{GOVERNING_LAW}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Disclosing Party</div>
|
||||
<div class="party-name">{{DISCLOSING_PARTY_NAME}}</div>
|
||||
<div class="party-detail">{{DISCLOSING_PARTY_ADDRESS}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Receiving Party</div>
|
||||
<div class="party-name">{{RECEIVING_PARTY_NAME}}</div>
|
||||
<div class="party-detail">{{RECEIVING_PARTY_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal Sections -->
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Purpose</div>
|
||||
<div class="legal-section-body">{{PURPOSE_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Definitions</div>
|
||||
<div class="legal-section-body">{{DEFINITION_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Obligations of Receiving Party</div>
|
||||
<div class="legal-section-body">{{OBLIGATIONS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Exclusions</div>
|
||||
<div class="legal-section-body">{{EXCLUSIONS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Term</div>
|
||||
<div class="legal-section-body">{{TERM_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Remedies</div>
|
||||
<div class="legal-section-body">{{REMEDIES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Return of Information</div>
|
||||
<div class="legal-section-body">{{RETURN_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">8. Intellectual Property</div>
|
||||
<div class="legal-section-body">{{INTELLECTUAL_PROPERTY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">9. No Licence</div>
|
||||
<div class="legal-section-body">{{NO_LICENSE_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">10. Waiver</div>
|
||||
<div class="legal-section-body">{{WAIVER_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">11. Severability</div>
|
||||
<div class="legal-section-body">{{SEVERABILITY_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">12. Entire Agreement</div>
|
||||
<div class="legal-section-body">{{ENTIRE_AGREEMENT_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">13. Amendments</div>
|
||||
<div class="legal-section-body">{{AMENDMENTS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">14. Notices</div>
|
||||
<div class="legal-section-body">{{NOTICES_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Disclosing Party</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{DISCLOSING_SIGNER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{DISCLOSING_SIGNER_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{DISCLOSING_SIGN_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Receiving Party</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{RECEIVING_SIGNER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{RECEIVING_SIGNER_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{RECEIVING_SIGN_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,130 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Non-Disclosure Agreement</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 3pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.legal-section { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.legal-section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.legal-section-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .legal-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Non-Disclosure Agreement</div>
|
||||
<div class="doc-subtitle">Mutual NDA</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">1 February 2026</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">England and Wales</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Disclosing Party</div>
|
||||
<div class="party-name">Kestrel Technologies Ltd</div>
|
||||
<div class="party-detail">14 Innovation Drive, Cambridge, CB1 3AA, UK</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Receiving Party</div>
|
||||
<div class="party-name">Moorfield Capital Partners LLP</div>
|
||||
<div class="party-detail">3 King Street, London, EC2V 8BD, UK</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Agreement</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Purpose</div>
|
||||
<div class="legal-section-body">The parties wish to explore a potential commercial collaboration relating to the development and commercialisation of proprietary software products. Each party may disclose Confidential Information to the other solely for the purpose of evaluating this potential partnership.</div>
|
||||
</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Definitions</div>
|
||||
<div class="legal-section-body">"Confidential Information" means any information disclosed by one party to the other, whether orally, in writing, or by any other means, that is designated as confidential or that reasonably should be understood to be confidential given the nature of the information and the circumstances of disclosure.</div>
|
||||
</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Obligations of Receiving Party</div>
|
||||
<div class="legal-section-body">The Receiving Party shall: (a) hold all Confidential Information in strict confidence; (b) not disclose Confidential Information to any third party without the prior written consent of the Disclosing Party; (c) use Confidential Information solely for the Purpose; and (d) protect Confidential Information using at least the same degree of care it uses to protect its own confidential information, but in no event less than reasonable care.</div>
|
||||
</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Exclusions</div>
|
||||
<div class="legal-section-body">Obligations do not apply to information that: (a) is or becomes publicly known through no breach of this Agreement; (b) was rightfully in the Receiving Party's possession before disclosure; (c) is independently developed by the Receiving Party without use of Confidential Information; or (d) is required to be disclosed by law, regulation, or court order.</div>
|
||||
</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Term</div>
|
||||
<div class="legal-section-body">This Agreement shall remain in effect for three (3) years from the Effective Date, unless earlier terminated by either party on thirty (30) days' written notice. Confidentiality obligations shall survive termination for a further two (2) years.</div>
|
||||
</div>
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Remedies</div>
|
||||
<div class="legal-section-body">Each party acknowledges that breach of this Agreement may cause irreparable harm for which monetary damages would be an inadequate remedy. Accordingly, the Disclosing Party shall be entitled to seek injunctive or other equitable relief in addition to all other remedies available at law.</div>
|
||||
</div>
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Disclosing Party</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Jonathan Kestrel</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Chief Executive Officer</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 1 February 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Receiving Party</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Priya Moorfield</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Managing Partner</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 1 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,215 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Offer of Employment</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Candidate name ── */
|
||||
.candidate-name {
|
||||
font-size: 16pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin: 10pt 0 4pt;
|
||||
}
|
||||
|
||||
.opening-para {
|
||||
font-size: 10pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 3pt;
|
||||
margin-bottom: 8pt;
|
||||
border-left: 2pt solid var(--theme-accent, #2563eb);
|
||||
padding-left: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Body sections ── */
|
||||
.text-section {
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Acceptance deadline ── */
|
||||
.deadline-line {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin: 8pt 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{DOC_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Candidate -->
|
||||
<div class="candidate-name">{{CANDIDATE_NAME}}</div>
|
||||
<div class="opening-para">We are delighted to extend this offer of employment and look forward to welcoming you to our team.</div>
|
||||
|
||||
<!-- Role Details -->
|
||||
<div class="section-heading">Role Details</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Position:</span> <span class="meta-value">{{POSITION_TITLE}}</span>
|
||||
<span class="meta-label">Department:</span> <span class="meta-value">{{DEPARTMENT}}</span>
|
||||
<span class="meta-label">Start Date:</span> <span class="meta-value">{{START_DATE}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Compensation -->
|
||||
<div class="section-heading">Compensation</div>
|
||||
<div class="text-section">{{COMPENSATION_TEXT}}</div>
|
||||
|
||||
<!-- Benefits -->
|
||||
<div class="section-heading">Benefits</div>
|
||||
<div class="text-section">{{BENEFITS_TEXT}}</div>
|
||||
|
||||
<!-- Terms -->
|
||||
<div class="section-heading">Terms and Conditions</div>
|
||||
<div class="text-section">{{TERMS_TEXT}}</div>
|
||||
|
||||
<!-- Acceptance -->
|
||||
<div class="deadline-line">Please sign and return this letter by <strong>{{ACCEPTANCE_DEADLINE}}</strong> to confirm your acceptance of this offer.</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">On Behalf of {{ORG_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{SIGNER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{SIGNER_TITLE}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Candidate Acceptance</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{CANDIDATE_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,96 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Offer of Employment</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.candidate-name { font-size: 16pt; font-weight: 700; color: var(--theme-heading); margin: 10pt 0 4pt; }
|
||||
.opening-para { font-size: 10pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 8pt; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 3pt; margin-bottom: 8pt; border-left: 2pt solid var(--theme-accent); padding-left: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 9pt; }
|
||||
.meta-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.text-section { font-size: 9.5pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 4pt; }
|
||||
.deadline-line { font-size: 9pt; color: var(--theme-text); margin: 8pt 0; font-style: italic; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Offer Letter</div>
|
||||
<div class="doc-subtitle">Redwood Analytics Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="candidate-name">Sophie Hargreaves</div>
|
||||
<div class="opening-para">We are delighted to extend this offer of employment and look forward to welcoming you to our team.</div>
|
||||
<div class="section-heading">Role Details</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Position:</span> <span class="meta-value">Senior Data Analyst</span>
|
||||
<span class="meta-label">Department:</span> <span class="meta-value">Product Intelligence</span>
|
||||
<span class="meta-label">Start Date:</span> <span class="meta-value">2 March 2026</span>
|
||||
</div>
|
||||
<div class="section-heading">Compensation</div>
|
||||
<div class="text-section">Your annual base salary will be £58,000, paid monthly in arrears on the last working day of each month. You will be eligible for an annual performance bonus of up to 15% of your base salary, subject to the achievement of agreed targets. A salary review will take place each April, with any adjustment effective from 1 April.</div>
|
||||
<div class="section-heading">Benefits</div>
|
||||
<div class="text-section">You will be entitled to 25 days' annual leave per year, plus UK public holidays. The company operates a contributory pension scheme (employer contribution: 5%, employee minimum: 3%). Additional benefits include private health insurance (BUPA), a cycle-to-work scheme, and access to our employee assistance programme.</div>
|
||||
<div class="section-heading">Terms and Conditions</div>
|
||||
<div class="text-section">Employment is subject to satisfactory completion of a three-month probationary period, receipt of two satisfactory references, and evidence of your right to work in the United Kingdom. This offer is also conditional on a satisfactory Disclosure and Barring Service (DBS) check where applicable. Full terms and conditions are set out in the contract of employment enclosed with this letter.</div>
|
||||
<div class="deadline-line">Please sign and return this letter by <strong>28 February 2026</strong> to confirm your acceptance of this offer.</div>
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">On Behalf of Redwood Analytics Ltd</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Marcus Webb</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: Head of People & Culture</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Candidate Acceptance</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Sophie Hargreaves</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,134 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Memorandum</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 80pt auto;
|
||||
row-gap: 4pt;
|
||||
margin: 10pt 0;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-bottom: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
padding: 8pt 0;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 700;
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Body text ── */
|
||||
.memo-body {
|
||||
font-size: 10pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Memorandum</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">To:</span> <span class="meta-value">{{MEMO_TO}}</span>
|
||||
<span class="meta-label">From:</span> <span class="meta-value">{{MEMO_FROM}}</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{MEMO_DATE}}</span>
|
||||
<span class="meta-label">Subject:</span> <span class="meta-value">{{MEMO_SUBJECT}}</span>
|
||||
<span class="meta-label">CC:</span> <span class="meta-value">{{MEMO_CC}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="memo-body">{{MEMO_BODY}}</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div class="section-heading">Attachments</div>
|
||||
<div class="text-section">{{MEMO_ATTACHMENTS}}</div>
|
||||
|
||||
<!-- Closing -->
|
||||
<div class="section-heading">Closing</div>
|
||||
<div class="text-section">{{MEMO_CLOSING}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Memorandum</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.meta-grid { display: grid; grid-template-columns: 80pt auto; row-gap: 4pt; margin: 10pt 0; border-top: 0.5pt solid var(--theme-border); border-bottom: 0.5pt solid var(--theme-border); padding: 8pt 0; }
|
||||
.meta-label { font-weight: 700; font-size: 9pt; color: var(--theme-text); }
|
||||
.meta-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.memo-body { font-size: 10pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.text-section { font-size: 8.5pt; line-height: 1.5; margin-bottom: 4pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Memorandum</div>
|
||||
<div class="doc-subtitle">Ashford & Partners LLP</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">To:</span> <span class="meta-value">All Senior Associates and Partners</span>
|
||||
<span class="meta-label">From:</span> <span class="meta-value">Catherine Ashford, Managing Partner</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">19 February 2026</span>
|
||||
<span class="meta-label">Subject:</span> <span class="meta-value">Updated Remote Working Policy — Effective 1 March 2026</span>
|
||||
<span class="meta-label">CC:</span> <span class="meta-value">HR Department; Office Manager</span>
|
||||
</div>
|
||||
<div class="memo-body">Following the firm-wide review conducted in January, we are pleased to confirm a revised remote working policy that will take effect on 1 March 2026.
|
||||
|
||||
All fee-earners and support staff at Band 3 and above are eligible to work remotely for up to two days per week, provided that client-facing commitments and team meeting schedules are maintained. Requests must be agreed with your supervising partner at least one week in advance.
|
||||
|
||||
Please be aware that the following expectations remain unchanged regardless of work location: billable hour targets, client response times (within four business hours), and mandatory attendance at the monthly all-hands meeting held on the first Monday of each month.
|
||||
|
||||
Detailed guidance, including the updated remote-working agreement form, is available on the intranet under HR > Policies > Remote Working. Please complete and return the form to HR no later than 21 February 2026.
|
||||
|
||||
Questions should be directed to Gemma Clarke in Human Resources.</div>
|
||||
<div class="section-heading">Attachments</div>
|
||||
<div class="text-section">1. Remote Working Agreement Form (Rev. 3, Feb 2026) 2. IT Security Guidelines for Remote Access</div>
|
||||
<div class="section-heading">Closing</div>
|
||||
<div class="text-section">We appreciate your cooperation and look forward to a smooth transition. Please do not hesitate to contact HR if you have any questions.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,346 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pay Stub</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Employee info card ── */
|
||||
.employee-card {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-radius: 3pt;
|
||||
padding: 8pt 10pt;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.employee-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4pt 20pt;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 4pt;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8pt;
|
||||
min-width: 76pt;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 0 0 5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Pay columns ── */
|
||||
.pay-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14pt;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pay-col-inner {
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Pay tables ── */
|
||||
.pay-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 8pt;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pay-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pay-table thead th {
|
||||
padding: 4pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 7.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.pay-table thead th.num { text-align: right; }
|
||||
|
||||
.pay-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.pay-table tbody td {
|
||||
padding: 3pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.pay-table tbody td.num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pay-table tfoot tr {
|
||||
border-top: 1pt solid var(--theme-border, #e2e8f0);
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.pay-table tfoot td {
|
||||
padding: 4pt 6pt;
|
||||
font-weight: 700;
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.pay-table tfoot td.num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Net pay banner ── */
|
||||
.net-pay-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
padding: 9pt 14pt;
|
||||
margin: 0 0 10pt;
|
||||
}
|
||||
|
||||
.net-pay-label {
|
||||
font-size: 11pt;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.net-pay-amount {
|
||||
font-size: 20pt;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── YTD summary ── */
|
||||
.ytd-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.ytd-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.ytd-table thead th {
|
||||
padding: 4pt 8pt;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
font-size: 8pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.ytd-table thead th:first-child { text-align: left; }
|
||||
|
||||
.ytd-table tbody td {
|
||||
padding: 4pt 8pt;
|
||||
text-align: right;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.ytd-table tbody td:first-child {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
/* ── Payment details ── */
|
||||
.payment-info {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.net-pay-box { break-inside: avoid; page-break-inside: avoid; }
|
||||
.pay-cols { break-inside: avoid; page-break-inside: avoid; }
|
||||
.ytd-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.employee-card { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Pay Stub</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Employee Information -->
|
||||
<div class="employee-card">
|
||||
<div class="employee-card-grid">
|
||||
<div class="info-row"><span class="info-label">Employee Name:</span><span class="info-value">{{EMPLOYEE_NAME}}</span></div>
|
||||
<div class="info-row"><span class="info-label">Pay Period:</span><span class="info-value">{{PAY_PERIOD_START}} – {{PAY_PERIOD_END}}</span></div>
|
||||
<div class="info-row"><span class="info-label">Employee ID:</span><span class="info-value">{{EMPLOYEE_ID}}</span></div>
|
||||
<div class="info-row"><span class="info-label">Pay Date:</span><span class="info-value">{{PAY_DATE}}</span></div>
|
||||
<div class="info-row"><span class="info-label">Department:</span><span class="info-value">{{DEPARTMENT}}</span></div>
|
||||
<div class="info-row"><span class="info-label">Position:</span><span class="info-value">{{POSITION}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Earnings & Deductions -->
|
||||
<div class="pay-cols">
|
||||
<div class="pay-col-inner">
|
||||
<div class="section-heading">Earnings</div>
|
||||
<table class="pay-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="num">Hours</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{EARNINGS_ROWS}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">Gross Pay</td>
|
||||
<td class="num">{{GROSS_PAY}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pay-col-inner">
|
||||
<div class="section-heading">Deductions</div>
|
||||
<table class="pay-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{DEDUCTIONS_ROWS}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Total Deductions</td>
|
||||
<td class="num">{{TOTAL_DEDUCTIONS}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Net Pay -->
|
||||
<div class="net-pay-box">
|
||||
<div class="net-pay-label">Net Pay</div>
|
||||
<div class="net-pay-amount">{{NET_PAY}}</div>
|
||||
</div>
|
||||
|
||||
<!-- YTD Summary -->
|
||||
<div class="section-heading" style="margin-top: 10pt">Year-to-Date Summary</div>
|
||||
<table class="ytd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Gross Pay</th>
|
||||
<th>Total Deductions</th>
|
||||
<th>Net Pay</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>YTD</td>
|
||||
<td>{{YTD_GROSS}}</td>
|
||||
<td>{{YTD_DEDUCTIONS}}</td>
|
||||
<td>{{YTD_NET}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Payment Details -->
|
||||
<div class="section-heading" style="margin-top: 10pt">Payment Details</div>
|
||||
<div class="payment-info">{{PAYMENT_METHOD}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,184 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pay Stub</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.employee-card { background: var(--theme-surface); border: 0.5pt solid var(--theme-border); border-radius: 3pt; padding: 8pt 10pt; margin-bottom: 10pt; }
|
||||
.employee-card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4pt 20pt; }
|
||||
.info-row { display: flex; gap: 4pt; }
|
||||
.info-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8pt; min-width: 76pt; flex-shrink: 0; }
|
||||
.info-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 0 0 5pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.pay-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 14pt; margin-bottom: 0; }
|
||||
.pay-col-inner { margin-bottom: 10pt; }
|
||||
.pay-table { width: 100%; border-collapse: collapse; font-size: 8pt; }
|
||||
.pay-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.pay-table thead th { padding: 4pt 6pt; text-align: left; font-weight: 600; font-size: 7.5pt; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.pay-table thead th.num { text-align: right; }
|
||||
.pay-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.pay-table tbody td { padding: 3pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.pay-table tbody td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.pay-table tfoot tr { border-top: 1pt solid var(--theme-border); background: var(--theme-surface); }
|
||||
.pay-table tfoot td { padding: 4pt 6pt; font-weight: 700; font-size: 8.5pt; color: var(--theme-heading); }
|
||||
.pay-table tfoot td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.net-pay-box { display: flex; justify-content: space-between; align-items: center; background: var(--theme-primary); color: #fff; padding: 9pt 14pt; margin: 0 0 10pt; }
|
||||
.net-pay-label { font-size: 11pt; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; }
|
||||
.net-pay-amount { font-size: 20pt; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.ytd-table { width: 100%; border-collapse: collapse; font-size: 8.5pt; margin-bottom: 8pt; }
|
||||
.ytd-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.ytd-table thead th { padding: 4pt 8pt; text-align: right; font-weight: 600; font-size: 8pt; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
.ytd-table thead th:first-child { text-align: left; }
|
||||
.ytd-table tbody td { padding: 4pt 8pt; text-align: right; border-bottom: 0.3pt solid var(--theme-border); font-variant-numeric: tabular-nums; }
|
||||
.ytd-table tbody td:first-child { text-align: left; font-weight: 600; color: var(--theme-text-muted); }
|
||||
.payment-info { font-size: 8.5pt; color: var(--theme-text); line-height: 1.6; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Pay Stub</div>
|
||||
<div class="doc-subtitle">Meridian Technologies Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="employee-card">
|
||||
<div class="employee-card-grid">
|
||||
<div class="info-row"><span class="info-label">Employee Name:</span><span class="info-value">Sarah J. Mitchell</span></div>
|
||||
<div class="info-row"><span class="info-label">Pay Period:</span><span class="info-value">1 February 2026 – 28 February 2026</span></div>
|
||||
<div class="info-row"><span class="info-label">Employee ID:</span><span class="info-value">EMP-0142</span></div>
|
||||
<div class="info-row"><span class="info-label">Pay Date:</span><span class="info-value">28 February 2026</span></div>
|
||||
<div class="info-row"><span class="info-label">Department:</span><span class="info-value">Engineering</span></div>
|
||||
<div class="info-row"><span class="info-label">Position:</span><span class="info-value">Senior Software Engineer</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-cols">
|
||||
<div class="pay-col-inner">
|
||||
<div class="section-heading">Earnings</div>
|
||||
<table class="pay-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="num">Hours</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Basic Salary</td>
|
||||
<td class="num">160</td>
|
||||
<td class="num">£30.21</td>
|
||||
<td class="num">£4,833.33</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Overtime</td>
|
||||
<td class="num">8</td>
|
||||
<td class="num">£45.31</td>
|
||||
<td class="num">£362.50</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>On-Call Allowance</td>
|
||||
<td class="num">—</td>
|
||||
<td class="num">—</td>
|
||||
<td class="num">£150.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">Gross Pay</td>
|
||||
<td class="num">£5,345.83</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pay-col-inner">
|
||||
<div class="section-heading">Deductions</div>
|
||||
<table class="pay-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Income Tax (PAYE)</td>
|
||||
<td class="num">£1,156.60</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>National Insurance</td>
|
||||
<td class="num">£455.62</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pension (Employee 5%)</td>
|
||||
<td class="num">£267.29</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Health Insurance</td>
|
||||
<td class="num">£85.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Total Deductions</td>
|
||||
<td class="num">£1,964.51</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="net-pay-box">
|
||||
<div class="net-pay-label">Net Pay</div>
|
||||
<div class="net-pay-amount">£3,381.32</div>
|
||||
</div>
|
||||
<div class="section-heading" style="margin-top:10pt">Year-to-Date Summary</div>
|
||||
<table class="ytd-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Gross Pay</th>
|
||||
<th>Total Deductions</th>
|
||||
<th>Net Pay</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>YTD</td>
|
||||
<td>£10,524.16</td>
|
||||
<td>£3,871.42</td>
|
||||
<td>£6,652.74</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading" style="margin-top:10pt">Payment Details</div>
|
||||
<div class="payment-info">Payment Method: BACS Direct Credit<br>Bank: Lloyds Bank plc | Sort Code: 30-00-00 | Account: ****4821</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,316 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Performance Review</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Review info grid ── */
|
||||
.review-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin-bottom: 10pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.review-cell {
|
||||
padding: 4pt 7pt;
|
||||
border-bottom: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-right: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.review-cell:nth-child(even) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.review-cell:nth-last-child(-n+2) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 7.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
margin-bottom: 1pt;
|
||||
}
|
||||
|
||||
.cell-value {
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Rating scale note ── */
|
||||
.rating-scale-note {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-style: italic;
|
||||
margin-bottom: 6pt;
|
||||
padding: 4pt 6pt;
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
border-left: 2pt solid var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Competency table ── */
|
||||
.competency-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.competency-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.competency-table thead th {
|
||||
padding: 4pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.competency-table thead th.center { text-align: center; }
|
||||
|
||||
.competency-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.competency-table tbody td {
|
||||
padding: 3pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.competency-table tbody td.center { text-align: center; }
|
||||
|
||||
/* ── Overall rating box ── */
|
||||
.overall-rating-box {
|
||||
display: inline-block;
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
font-size: 11pt;
|
||||
font-weight: 700;
|
||||
padding: 5pt 14pt;
|
||||
border-radius: 2pt;
|
||||
margin: 4pt 0 8pt;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.ack-note {
|
||||
font-size: 8.5pt;
|
||||
font-style: italic;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Employee Performance Review</div>
|
||||
<div class="doc-subtitle">{{ORG_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Review Info Grid -->
|
||||
<div class="section-heading">Review Information</div>
|
||||
<div class="review-grid">
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Employee Name</div>
|
||||
<div class="cell-value">{{EMPLOYEE_NAME}}</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Employee ID</div>
|
||||
<div class="cell-value">{{EMPLOYEE_ID}}</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Job Title</div>
|
||||
<div class="cell-value">{{JOB_TITLE}}</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Department</div>
|
||||
<div class="cell-value">{{DEPARTMENT}}</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Manager</div>
|
||||
<div class="cell-value">{{MANAGER_NAME}}</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Review Period</div>
|
||||
<div class="cell-value">{{REVIEW_PERIOD_START}} to {{REVIEW_PERIOD_END}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating Scale -->
|
||||
<div class="rating-scale-note">{{RATING_SCALE_TEXT}}</div>
|
||||
|
||||
<!-- Goals & Outcomes -->
|
||||
<div class="section-heading">Goals and Outcomes</div>
|
||||
<div class="text-section"><strong>Goals:</strong><br>{{GOALS_TEXT}}</div>
|
||||
<div class="text-section"><strong>Outcomes:</strong><br>{{GOAL_OUTCOMES_TEXT}}</div>
|
||||
|
||||
<!-- Competencies Table -->
|
||||
<div class="section-heading">Competencies and Ratings</div>
|
||||
<table class="competency-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45%">Competency</th>
|
||||
<th class="center" style="width:15%">Rating (1–5)</th>
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{COMPETENCY_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Key Accomplishments -->
|
||||
<div class="section-heading">Key Accomplishments</div>
|
||||
<div class="text-section">{{ACCOMPLISHMENTS_TEXT}}</div>
|
||||
|
||||
<!-- Areas for Improvement -->
|
||||
<div class="section-heading">Areas for Improvement</div>
|
||||
<div class="text-section">{{IMPROVEMENT_TEXT}}</div>
|
||||
|
||||
<!-- Development Plan -->
|
||||
<div class="section-heading">Development Plan</div>
|
||||
<div class="text-section">{{DEVELOPMENT_PLAN_TEXT}}</div>
|
||||
|
||||
<!-- Overall Rating -->
|
||||
<div class="section-heading">Overall Rating</div>
|
||||
<div class="overall-rating-box">{{OVERALL_RATING}}</div>
|
||||
|
||||
<!-- Employee Comments -->
|
||||
<div class="section-heading">Employee Comments</div>
|
||||
<div class="text-section">{{EMPLOYEE_COMMENTS_TEXT}}</div>
|
||||
|
||||
<!-- Manager Comments -->
|
||||
<div class="section-heading">Manager Comments</div>
|
||||
<div class="text-section">{{MANAGER_COMMENTS_TEXT}}</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Acknowledgement</div>
|
||||
<div class="ack-note">Signatures below acknowledge receipt and discussion of this review. Signature does not necessarily indicate agreement with all conclusions.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Manager</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{MANAGER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{MANAGER_SIGN_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{EMPLOYEE_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{EMPLOYEE_SIGN_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,177 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Performance Review</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.review-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border: 0.5pt solid var(--theme-border); margin-bottom: 10pt; font-size: 8.5pt; }
|
||||
.review-cell { padding: 4pt 7pt; border-bottom: 0.5pt solid var(--theme-border); border-right: 0.5pt solid var(--theme-border); }
|
||||
.review-cell:nth-child(even) { border-right: none; }
|
||||
.review-cell:nth-last-child(-n+2) { border-bottom: none; }
|
||||
.cell-label { font-weight: 600; color: var(--theme-text-muted); font-size: 7.5pt; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 1pt; }
|
||||
.cell-value { color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.rating-scale-note { font-size: 8pt; color: var(--theme-text-muted); font-style: italic; margin-bottom: 6pt; padding: 4pt 6pt; background: var(--theme-surface); border-left: 2pt solid var(--theme-accent); }
|
||||
.text-section { font-size: 9.5pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 4pt; white-space: pre-line; }
|
||||
.competency-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8pt; }
|
||||
.competency-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.competency-table thead th { padding: 4pt 6pt; text-align: left; font-weight: 600; }
|
||||
.competency-table thead th.center { text-align: center; }
|
||||
.competency-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.competency-table tbody td { padding: 3pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.competency-table tbody td.center { text-align: center; }
|
||||
.overall-rating-box { display: inline-block; background: var(--theme-primary); color: #fff; font-size: 11pt; font-weight: 700; padding: 5pt 14pt; border-radius: 2pt; margin: 4pt 0 8pt; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
.ack-note { font-size: 8.5pt; font-style: italic; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Performance Review</div>
|
||||
<div class="doc-subtitle">Cavendish Financial Solutions Ltd</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Review Information</div>
|
||||
<div class="review-grid">
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Employee Name</div>
|
||||
<div class="cell-value">Priya Nair</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Employee ID</div>
|
||||
<div class="cell-value">EMP-2847</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Job Title</div>
|
||||
<div class="cell-value">Senior Business Analyst</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Department</div>
|
||||
<div class="cell-value">Strategy & Operations</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Manager</div>
|
||||
<div class="cell-value">Jonathan Ashby</div>
|
||||
</div>
|
||||
<div class="review-cell">
|
||||
<div class="cell-label">Review Period</div>
|
||||
<div class="cell-value">1 January 2025 to 31 December 2025</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rating-scale-note">Rating scale: 1 = Does Not Meet Expectations | 2 = Partially Meets Expectations | 3 = Meets Expectations | 4 = Exceeds Expectations | 5 = Outstanding</div>
|
||||
<div class="section-heading">Goals and Outcomes</div>
|
||||
<div class="text-section"><strong>Goals:</strong><br>1. Lead the migration of legacy reporting infrastructure to the new data platform by Q3 2025.
|
||||
2. Achieve a client satisfaction score of 4.5 or above across assigned accounts.
|
||||
3. Complete the Chartered Business Analysis Professional (CBAP) certification.</div>
|
||||
<div class="text-section"><strong>Outcomes:</strong><br>The data platform migration was completed two weeks ahead of schedule in August 2025, with zero production incidents during cutover. Client satisfaction scores averaged 4.7 across all assigned accounts. Priya successfully attained her CBAP certification in October 2025.</div>
|
||||
<div class="section-heading">Competencies and Ratings</div>
|
||||
<table class="competency-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45%">Competency</th>
|
||||
<th class="center" style="width:15%">Rating (1–5)</th>
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Problem Solving & Analysis</td>
|
||||
<td class="center">5</td>
|
||||
<td>Consistently applies structured methodologies; proactively identifies root causes.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Communication & Stakeholder Management</td>
|
||||
<td class="center">4</td>
|
||||
<td>Clear and concise communicator; builds strong relationships across all levels.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delivery & Results Orientation</td>
|
||||
<td class="center">5</td>
|
||||
<td>Delivered all major workstreams on time or ahead of schedule throughout the year.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Teamwork & Collaboration</td>
|
||||
<td class="center">4</td>
|
||||
<td>A valued team member who readily supports colleagues and shares knowledge.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Adaptability & Learning</td>
|
||||
<td class="center">5</td>
|
||||
<td>Embraced new tooling and methodology changes with notable enthusiasm.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading">Key Accomplishments</div>
|
||||
<div class="text-section">Successfully led a cross-functional team of eight in delivering the data platform migration, generating an estimated annual saving of £180,000 in licensing and maintenance costs. Authored a new requirements management framework that has since been adopted company-wide. Mentored two junior analysts, both of whom achieved promotion to Analyst II during the review period.</div>
|
||||
<div class="section-heading">Areas for Improvement</div>
|
||||
<div class="text-section">Delegation: Priya occasionally takes on too much individually rather than distributing work effectively across the team. Developing stronger delegation habits will be critical as she moves towards a lead role.
|
||||
|
||||
Presentation skills: While written communication is excellent, further development of executive-level presentation delivery would strengthen impact with senior stakeholders.</div>
|
||||
<div class="section-heading">Development Plan</div>
|
||||
<div class="text-section">1. Attend the internal Leadership Essentials programme (Q1 2026).
|
||||
2. Co-present at the annual All-Hands strategy review in March 2026 alongside the Head of Strategy.
|
||||
3. Shadow the Delivery Director on at least two client pitches before Q2 2026.
|
||||
4. Set up a fortnightly one-to-one with direct reports to strengthen delegation practices.</div>
|
||||
<div class="section-heading">Overall Rating</div>
|
||||
<div class="overall-rating-box">4.6 — Exceeds Expectations</div>
|
||||
<div class="section-heading">Employee Comments</div>
|
||||
<div class="text-section">I am proud of what the team achieved with the platform migration this year and grateful for the support and autonomy Jonathan provided throughout. I welcome the feedback on delegation and will prioritise the Leadership Essentials programme. I am excited about the development opportunities outlined for 2026.</div>
|
||||
<div class="section-heading">Manager Comments</div>
|
||||
<div class="text-section">Priya has had an outstanding year and is one of the most effective contributors in the Strategy & Operations team. Her technical depth, client focus, and commitment to professional development set an excellent example. I look forward to supporting her continued growth into a senior leadership position in 2026.</div>
|
||||
<div class="section-heading">Acknowledgement</div>
|
||||
<div class="ack-note">Signatures below acknowledge receipt and discussion of this review. Signature does not necessarily indicate agreement with all conclusions.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Manager</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Jonathan Ashby</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 15 January 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Priya Nair</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 16 January 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,185 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Price Sheet</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta info grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Price table ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num { text-align: right; }
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table .desc-col { width: 38%; }
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{PRICE_SHEET_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Effective Date</div>
|
||||
<div class="doc-number">{{EFFECTIVE_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Currency:</span> <span class="meta-value">{{CURRENCY}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Pricing Table -->
|
||||
<div class="section-heading">Pricing</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:26pt">SKU</th>
|
||||
<th class="desc-col">Description</th>
|
||||
<th style="width:48pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{PRICE_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">{{NOTES_TEXT}}</div>
|
||||
|
||||
<!-- Terms -->
|
||||
<div class="section-heading">Terms</div>
|
||||
<div class="text-section">{{TERMS_TEXT}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,143 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Price Sheet</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; white-space: nowrap; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.text-section { font-size: 8.5pt; line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Price Sheet</div>
|
||||
<div class="doc-subtitle">Vantage Cloud Services Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Effective Date</div>
|
||||
<div class="doc-number">1 March 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Currency:</span> <span class="meta-value">GBP (£) — All prices exclude VAT</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Pricing</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:26pt">SKU</th>
|
||||
<th style="width:38%">Description</th>
|
||||
<th style="width:48pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>VCS-001</td>
|
||||
<td>Starter Plan — up to 5 users</td>
|
||||
<td>per month</td>
|
||||
<td class="num">£49.00</td>
|
||||
<td>Billed monthly; cancel anytime</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-002</td>
|
||||
<td>Growth Plan — up to 25 users</td>
|
||||
<td>per month</td>
|
||||
<td class="num">£149.00</td>
|
||||
<td>Includes advanced analytics</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-003</td>
|
||||
<td>Business Plan — up to 100 users</td>
|
||||
<td>per month</td>
|
||||
<td class="num">£399.00</td>
|
||||
<td>SSO & priority support included</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-004</td>
|
||||
<td>Enterprise Plan — unlimited users</td>
|
||||
<td>per month</td>
|
||||
<td class="num">£899.00</td>
|
||||
<td>Dedicated account manager</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-010</td>
|
||||
<td>Annual licence discount (any plan)</td>
|
||||
<td>per year</td>
|
||||
<td class="num">−20%</td>
|
||||
<td>Pay annually, save 20%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-020</td>
|
||||
<td>Professional services — implementation</td>
|
||||
<td>per day</td>
|
||||
<td class="num">£1,200.00</td>
|
||||
<td>Min. 1 day; remote or on-site</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-021</td>
|
||||
<td>Training workshop (half-day, up to 10 delegates)</td>
|
||||
<td>per session</td>
|
||||
<td class="num">£650.00</td>
|
||||
<td>Remote delivery only</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VCS-030</td>
|
||||
<td>Additional data storage</td>
|
||||
<td>per 100 GB/month</td>
|
||||
<td class="num">£18.00</td>
|
||||
<td>First 50 GB included in plan</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">All prices are quoted in GBP and exclude VAT at the prevailing rate. Custom enterprise pricing is available for organisations requiring more than 500 users or bespoke SLA terms — please contact sales@vantagecloud.co.uk for a tailored quote. Volume discounts of up to 15% are available for multi-year commitments.</div>
|
||||
<div class="section-heading">Terms</div>
|
||||
<div class="text-section">Subscriptions are invoiced monthly in advance unless an annual plan is selected. Payment is due within 14 days of invoice. Prices are subject to annual review; customers will be notified no less than 60 days before any change takes effect. All plans are subject to Vantage Cloud Services Standard Terms and Conditions (available at vantagecloud.co.uk/terms).</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,377 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{DOCUMENT_TITLE}}</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm 20mm 20mm 20mm;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ── Document wrapper ── */
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta info grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Parties grid ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-card {
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-radius: 3pt;
|
||||
padding: 6pt 8pt;
|
||||
}
|
||||
|
||||
.party-card-header {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Addresses row ── */
|
||||
.addresses-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
/* ── Line items table ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table .desc-col {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
/* ── Totals block ── */
|
||||
.totals-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.totals-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
min-width: 140pt;
|
||||
}
|
||||
|
||||
.totals-table td {
|
||||
padding: 2pt 6pt;
|
||||
}
|
||||
|
||||
.totals-table td:first-child {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.totals-table td:last-child {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.totals-table .total-row td {
|
||||
font-weight: 700;
|
||||
font-size: 9.5pt;
|
||||
border-top: 1pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-top: 4pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
/* ── Notes / text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
/* ── Authorization ── */
|
||||
.auth-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
margin-top: 4pt;
|
||||
}
|
||||
|
||||
.auth-table td:first-child {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
width: 100pt;
|
||||
padding: 2pt 0;
|
||||
}
|
||||
|
||||
.auth-table td:last-child {
|
||||
padding: 2pt 6pt;
|
||||
}
|
||||
|
||||
/* ── Footer note ── */
|
||||
.footer-note {
|
||||
margin-top: 10pt;
|
||||
padding-top: 6pt;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.totals-wrapper { break-inside: avoid; page-break-inside: avoid; }
|
||||
.totals-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.party-card { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{DOCUMENT_TITLE}}</div>
|
||||
<div class="doc-subtitle">{{BUYER_NAME}}</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">PO Number</div>
|
||||
<div class="doc-number">{{PO_NUMBER}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">PO Date:</span> <span class="meta-value">{{PO_DATE}}</span>
|
||||
<span class="meta-label">Buyer Reference:</span> <span class="meta-value">{{BUYER_REFERENCE}}</span>
|
||||
<span class="meta-label">Vendor Reference:</span> <span class="meta-value">{{VENDOR_REFERENCE}}</span>
|
||||
<span class="meta-label">Currency:</span> <span class="meta-value">{{CURRENCY}}</span>
|
||||
<span class="meta-label">Requested Delivery:</span><span class="meta-value">{{REQUESTED_DELIVERY_DATE}}</span>
|
||||
<span class="meta-label">Delivery Method:</span> <span class="meta-value">{{DELIVERY_METHOD}}</span>
|
||||
<span class="meta-label">Incoterms:</span> <span class="meta-value">{{INCOTERMS}}</span>
|
||||
<span class="meta-label">Payment Terms:</span> <span class="meta-value">{{PAYMENT_TERMS}}</span>
|
||||
<span class="meta-label">Confidentiality:</span> <span class="meta-value">{{CONFIDENTIALITY_LABEL}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties & Addresses</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Buyer</div>
|
||||
<div class="party-name">{{BUYER_NAME}}</div>
|
||||
<div class="party-detail">{{BUYER_ADDRESS}}</div>
|
||||
<div class="party-detail">Contact: {{BUYER_CONTACT_NAME}}</div>
|
||||
<div class="party-detail">Email: {{BUYER_CONTACT_EMAIL}}</div>
|
||||
<div class="party-detail">Phone: {{BUYER_CONTACT_PHONE}}</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Vendor</div>
|
||||
<div class="party-name">{{VENDOR_NAME}}</div>
|
||||
<div class="party-detail">{{VENDOR_ADDRESS}}</div>
|
||||
<div class="party-detail">Contact: {{VENDOR_CONTACT_NAME}}</div>
|
||||
<div class="party-detail">Email: {{VENDOR_CONTACT_EMAIL}}</div>
|
||||
<div class="party-detail">Phone: {{VENDOR_CONTACT_PHONE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="addresses-row">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Bill To</div>
|
||||
<div class="party-name">{{BILL_TO_NAME}}</div>
|
||||
<div class="party-detail">{{BILL_TO_ADDRESS}}</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Ship To</div>
|
||||
<div class="party-name">{{SHIP_TO_NAME}}</div>
|
||||
<div class="party-detail">{{SHIP_TO_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="section-heading">Line Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:24pt">#</th>
|
||||
<th class="desc-col">Description</th>
|
||||
<th class="num" style="width:36pt">Qty</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{LINEITEM_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>{{SUBTOTAL_TEXT}}</td></tr>
|
||||
<tr><td>Tax:</td><td>{{TAX_TEXT}}</td></tr>
|
||||
<tr><td>Shipping:</td><td>{{SHIPPING_TEXT}}</td></tr>
|
||||
<tr><td>Discount:</td><td>{{DISCOUNT_TEXT}}</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>{{TOTAL_TEXT}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">{{NOTES_TEXT}}</div>
|
||||
|
||||
<!-- Special Instructions -->
|
||||
<div class="section-heading">Special Instructions</div>
|
||||
<div class="text-section">{{SPECIAL_INSTRUCTIONS_TEXT}}</div>
|
||||
|
||||
<!-- Terms -->
|
||||
<div class="section-heading">Terms and Conditions</div>
|
||||
<div class="text-section">{{TERMS_TEXT}}</div>
|
||||
|
||||
<!-- Authorization -->
|
||||
<div class="section-heading">Authorisation</div>
|
||||
<table class="auth-table">
|
||||
<tbody>
|
||||
<tr><td>Authorised by:</td><td>{{AUTHORIZED_BY}}</td></tr>
|
||||
<tr><td>Title:</td><td>{{AUTHORIZED_TITLE}}</td></tr>
|
||||
<tr><td>Date:</td><td>{{AUTHORIZED_DATE}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer-note">{{FOOTER_NOTE}}</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,195 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Purchase Order</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--theme-font);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text);
|
||||
background: var(--theme-bg);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.addresses-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-card { border: 0.5pt solid var(--theme-border); border-radius: 3pt; padding: 6pt 8pt; }
|
||||
.party-card-header { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 4pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.totals-wrapper { display: flex; justify-content: flex-end; margin-bottom: 10pt; }
|
||||
.totals-table { border-collapse: collapse; font-size: 8.5pt; min-width: 140pt; }
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
.totals-table td:first-child { color: var(--theme-text-muted); }
|
||||
.totals-table td:last-child { text-align: right; }
|
||||
.totals-table .total-row td { font-weight: 700; font-size: 9.5pt; border-top: 1pt solid var(--theme-primary); padding-top: 4pt; }
|
||||
.text-section { font-size: 8.5pt; line-height: 1.5; margin-bottom: 4pt; }
|
||||
.auth-table { width: 100%; border-collapse: collapse; font-size: 8.5pt; margin-top: 4pt; }
|
||||
.auth-table td:first-child { font-weight: 600; color: var(--theme-text-muted); width: 100pt; padding: 2pt 0; }
|
||||
.auth-table td:last-child { padding: 2pt 6pt; }
|
||||
.footer-note { margin-top: 10pt; padding-top: 6pt; border-top: 0.5pt solid var(--theme-border); font-size: 7.5pt; color: var(--theme-text-muted); }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Purchase Order</div>
|
||||
<div class="doc-subtitle">Sterling Systems Group Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">PO Number</div>
|
||||
<div class="doc-number">PO-2026-0184</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">PO Date:</span> <span class="meta-value">12 January 2026</span>
|
||||
<span class="meta-label">Buyer Reference:</span> <span class="meta-value">REQ-A19-772</span>
|
||||
<span class="meta-label">Vendor Reference:</span> <span class="meta-value">QUOTE-Q4481</span>
|
||||
<span class="meta-label">Currency:</span> <span class="meta-value">GBP</span>
|
||||
<span class="meta-label">Requested Delivery:</span><span class="meta-value">19 January 2026</span>
|
||||
<span class="meta-label">Delivery Method:</span> <span class="meta-value">Courier</span>
|
||||
<span class="meta-label">Incoterms:</span> <span class="meta-value">DAP (Delivered at Place)</span>
|
||||
<span class="meta-label">Payment Terms:</span> <span class="meta-value">Net 30</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Parties & Addresses</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Buyer</div>
|
||||
<div class="party-name">Sterling Systems Group Ltd</div>
|
||||
<div class="party-detail">100 Kingfisher Way, London, EC2V 7AB, UK</div>
|
||||
<div class="party-detail">Contact: Operations Procurement</div>
|
||||
<div class="party-detail">Email: procurement@example.com</div>
|
||||
<div class="party-detail">Phone: +44 20 0000 0000</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Vendor</div>
|
||||
<div class="party-name">Northbridge Office Supply Co.</div>
|
||||
<div class="party-detail">14 Meridian Park, Reading, RG1 1AA, UK</div>
|
||||
<div class="party-detail">Contact: Accounts Desk</div>
|
||||
<div class="party-detail">Email: orders@example.com</div>
|
||||
<div class="party-detail">Phone: +44 118 000 0000</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="addresses-row">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Bill To</div>
|
||||
<div class="party-name">Sterling Systems Group Ltd (Accounts Payable)</div>
|
||||
<div class="party-detail">100 Kingfisher Way, London, EC2V 7AB, UK</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Ship To</div>
|
||||
<div class="party-name">Sterling Systems Group Ltd (Receiving)</div>
|
||||
<div class="party-detail">Dock 2, 100 Kingfisher Way, London, EC2V 7AB, UK</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Line Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:24pt">#</th>
|
||||
<th style="width:45%">Description</th>
|
||||
<th class="num" style="width:36pt">Qty</th>
|
||||
<th style="width:36pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>A4 paper, 80gsm (box of 5 reams)</td>
|
||||
<td class="num">20</td>
|
||||
<td>box</td>
|
||||
<td class="num">£18.50</td>
|
||||
<td class="num">£370.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Black toner cartridge (Model X200)</td>
|
||||
<td class="num">6</td>
|
||||
<td>unit</td>
|
||||
<td class="num">£74.00</td>
|
||||
<td class="num">£444.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Heavy-duty archive boxes (pack of 10)</td>
|
||||
<td class="num">8</td>
|
||||
<td>pack</td>
|
||||
<td class="num">£21.25</td>
|
||||
<td class="num">£170.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>£984.00</td></tr>
|
||||
<tr><td>Tax (20% VAT):</td><td>£196.80</td></tr>
|
||||
<tr><td>Shipping:</td><td>£25.00</td></tr>
|
||||
<tr><td>Discount:</td><td>-£30.00</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>£1,175.80</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">Deliveries accepted Mon–Fri, 09:00–16:00. Please reference the PO number on all delivery documents.</div>
|
||||
<div class="section-heading">Special Instructions</div>
|
||||
<div class="text-section">Pack cartons securely; label each carton with PO number and delivery contact.</div>
|
||||
<div class="section-heading">Terms and Conditions</div>
|
||||
<div class="text-section">Payment per stated terms; substitutions require buyer approval; notify buyer of delays exceeding 48 hours.</div>
|
||||
<div class="section-heading">Authorisation</div>
|
||||
<table class="auth-table">
|
||||
<tbody>
|
||||
<tr><td>Authorised by:</td><td>Alexandra Moore</td></tr>
|
||||
<tr><td>Title:</td><td>Head of Operations</td></tr>
|
||||
<tr><td>Date:</td><td>12 January 2026</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer-note">This document is provided for demonstration purposes only.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,358 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quotation {{QUOTE_NUMBER}}</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta info grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
.meta-value.valid-until {
|
||||
font-weight: 600;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Party cards ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-card {
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-radius: 3pt;
|
||||
padding: 6pt 8pt;
|
||||
}
|
||||
|
||||
.party-card-header {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Line items table ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num { text-align: right; }
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table .desc-col { width: 40%; }
|
||||
|
||||
/* ── Totals block ── */
|
||||
.totals-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.totals-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
min-width: 150pt;
|
||||
}
|
||||
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
|
||||
.totals-table td:first-child {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.totals-table td:last-child {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.totals-table .total-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
border-top: 1pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-top: 4pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.totals-table .grand-row td {
|
||||
font-weight: 700;
|
||||
font-size: 11pt;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Acceptance box ── */
|
||||
.acceptance-box {
|
||||
border: 0.75pt solid var(--theme-border, #e2e8f0);
|
||||
border-radius: 3pt;
|
||||
padding: 8pt 10pt;
|
||||
margin-top: 10pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.acceptance-title {
|
||||
font-size: 9pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 6pt;
|
||||
}
|
||||
|
||||
.acceptance-text {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12pt;
|
||||
}
|
||||
|
||||
.acceptance-sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16pt;
|
||||
}
|
||||
|
||||
.acc-sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.acc-sig-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.totals-wrapper { break-inside: avoid; page-break-inside: avoid; }
|
||||
.totals-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.party-card { break-inside: avoid; page-break-inside: avoid; }
|
||||
.acceptance-box { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
.acceptance-box { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Quotation</div>
|
||||
<div class="doc-subtitle">{{VENDOR_NAME}}</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Quote Number</div>
|
||||
<div class="doc-number">{{QUOTE_NUMBER}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Quote Date:</span> <span class="meta-value">{{QUOTE_DATE}}</span>
|
||||
<span class="meta-label">Valid Until:</span> <span class="meta-value valid-until">{{VALID_UNTIL}}</span>
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">{{REFERENCE}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">From</div>
|
||||
<div class="party-name">{{VENDOR_NAME}}</div>
|
||||
<div class="party-detail">{{VENDOR_ADDRESS}}</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Quote To</div>
|
||||
<div class="party-name">{{CLIENT_NAME}}</div>
|
||||
<div class="party-detail">{{CLIENT_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="section-heading">Line Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22pt">#</th>
|
||||
<th class="desc-col">Description</th>
|
||||
<th class="num" style="width:32pt">Qty</th>
|
||||
<th style="width:32pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{LINEITEM_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>{{SUBTOTAL_TEXT}}</td></tr>
|
||||
<tr><td>Discount:</td><td>{{DISCOUNT_TEXT}}</td></tr>
|
||||
<tr><td>Tax:</td><td>{{TAX_TEXT}}</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>{{TOTAL_TEXT}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Terms -->
|
||||
<div class="section-heading">Terms & Conditions</div>
|
||||
<div class="text-section">{{TERMS_TEXT}}</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">{{NOTES_TEXT}}</div>
|
||||
|
||||
<!-- Acceptance -->
|
||||
<div class="acceptance-box">
|
||||
<div class="acceptance-title">Acceptance</div>
|
||||
<div class="acceptance-text">By signing below, the client accepts this quotation and authorises the work described herein. This quotation is valid until {{ACCEPTANCE_DEADLINE}}.</div>
|
||||
<div class="acceptance-sig-grid">
|
||||
<div>
|
||||
<div class="acc-sig-line"></div>
|
||||
<div class="acc-sig-label">Authorised Signature</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="acc-sig-line"></div>
|
||||
<div class="acc-sig-label">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,187 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quotation</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.meta-value.valid-until { font-weight: 600; color: var(--theme-accent); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-card { border: 0.5pt solid var(--theme-border); border-radius: 3pt; padding: 6pt 8pt; }
|
||||
.party-card-header { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 4pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; white-space: nowrap; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.totals-wrapper { display: flex; justify-content: flex-end; margin-bottom: 10pt; }
|
||||
.totals-table { border-collapse: collapse; font-size: 8.5pt; min-width: 150pt; }
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
.totals-table td:first-child { color: var(--theme-text-muted); }
|
||||
.totals-table td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.totals-table .total-row td { font-weight: 700; font-size: 10pt; border-top: 1pt solid var(--theme-primary); padding-top: 4pt; color: var(--theme-heading); }
|
||||
.text-section { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
.acceptance-box { border: 0.75pt solid var(--theme-border); border-radius: 3pt; padding: 8pt 10pt; margin-top: 10pt; page-break-inside: avoid; }
|
||||
.acceptance-title { font-size: 9pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6pt; }
|
||||
.acceptance-text { font-size: 8.5pt; color: var(--theme-text); line-height: 1.5; margin-bottom: 12pt; }
|
||||
.acceptance-sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16pt; }
|
||||
.acc-sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.acc-sig-label { font-size: 7.5pt; color: var(--theme-text-muted); }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .acceptance-box { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Quotation</div>
|
||||
<div class="doc-subtitle">Axiom Technology Solutions Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Quote Number</div>
|
||||
<div class="doc-number">QT-2026-0089</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Quote Date:</span> <span class="meta-value">19 February 2026</span>
|
||||
<span class="meta-label">Valid Until:</span> <span class="meta-value valid-until">19 March 2026</span>
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">BLP-IT-2026-Q3</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">From</div>
|
||||
<div class="party-name">Axiom Technology Solutions Ltd</div>
|
||||
<div class="party-detail">9 Silicon Way, Reading, RG1 1EH, UK</div>
|
||||
<div class="party-detail">VAT: GB456789012 · sales@axiomtech.co.uk</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Quote To</div>
|
||||
<div class="party-name">Brookfield Legal Partners LLP</div>
|
||||
<div class="party-detail">55 Temple Row, Birmingham, B2 5LU, UK</div>
|
||||
<div class="party-detail">Attn: Marcus Hale, IT Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Line Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22pt">#</th>
|
||||
<th style="width:40%">Description</th>
|
||||
<th class="num" style="width:32pt">Qty</th>
|
||||
<th style="width:32pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Dell OptiPlex 7020 Desktop (i7, 32 GB RAM, 512 GB SSD)</td>
|
||||
<td class="num">15</td>
|
||||
<td>unit</td>
|
||||
<td class="num">£849.00</td>
|
||||
<td class="num">£12,735.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Dell 27" UltraSharp Monitor U2724D</td>
|
||||
<td class="num">15</td>
|
||||
<td>unit</td>
|
||||
<td class="num">£389.00</td>
|
||||
<td class="num">£5,835.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Microsoft 365 Business Premium (annual subscription)</td>
|
||||
<td class="num">20</td>
|
||||
<td>licence</td>
|
||||
<td class="num">£220.00</td>
|
||||
<td class="num">£4,400.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>On-site setup, imaging, and deployment (per device)</td>
|
||||
<td class="num">15</td>
|
||||
<td>device</td>
|
||||
<td class="num">£75.00</td>
|
||||
<td class="num">£1,125.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>3-year on-site warranty extension (Tier 2)</td>
|
||||
<td class="num">15</td>
|
||||
<td>unit</td>
|
||||
<td class="num">£110.00</td>
|
||||
<td class="num">£1,650.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>£25,745.00</td></tr>
|
||||
<tr><td>Discount (5% volume):</td><td>−£1,287.25</td></tr>
|
||||
<tr><td>VAT (20%):</td><td>£4,891.55</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>£29,349.30</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Terms & Conditions</div>
|
||||
<div class="text-section">Payment is due within 30 days of invoice date. Hardware items ship within 5–7 business days of order confirmation. Software licences are delivered electronically within 24 hours. All hardware carries a minimum 12-month manufacturer warranty. Returns accepted within 14 days of delivery for unopened items only.</div>
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">Prices are inclusive of delivery to a single UK mainland address. Additional delivery locations are available at £35 per site. Volume pricing shown reflects a 5% discount applied to the hardware lines only. Quote is valid for 28 days from the date shown above. Please quote reference BLP-IT-2026-Q3 on all correspondence.</div>
|
||||
<div class="acceptance-box">
|
||||
<div class="acceptance-title">Acceptance</div>
|
||||
<div class="acceptance-text">By signing below, the client accepts this quotation and authorises the work described herein. This quotation is valid until 19 March 2026.</div>
|
||||
<div class="acceptance-sig-grid">
|
||||
<div>
|
||||
<div class="acc-sig-line"></div>
|
||||
<div class="acc-sig-label">Authorised Signature</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="acc-sig-line"></div>
|
||||
<div class="acc-sig-label">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,328 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Receipt {{RECEIPT_NUMBER}}</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Top accent bar ── */
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta info grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Party cards ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-card {
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-radius: 3pt;
|
||||
padding: 6pt 8pt;
|
||||
}
|
||||
|
||||
.party-card-header {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Line items table ── */
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 6pt;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.items-table thead tr {
|
||||
background: var(--theme-primary, #1e3a5f);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
padding: 5pt 6pt;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table thead th.num { text-align: right; }
|
||||
|
||||
.items-table tbody tr:nth-child(even) {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
padding: 4pt 6pt;
|
||||
border-bottom: 0.3pt solid var(--theme-border, #e2e8f0);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.items-table tbody td.num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-table .desc-col { width: 40%; }
|
||||
|
||||
/* ── Totals block ── */
|
||||
.totals-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.totals-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 8.5pt;
|
||||
min-width: 150pt;
|
||||
}
|
||||
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
|
||||
.totals-table td:first-child {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.totals-table td:last-child {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.totals-table .total-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
border-top: 1pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-top: 4pt;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.totals-table .received-row td {
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
}
|
||||
|
||||
/* ── Payment details grid ── */
|
||||
.payment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 3pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.payment-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.payment-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Issued by block ── */
|
||||
.issued-by-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 3pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.totals-wrapper { break-inside: avoid; page-break-inside: avoid; }
|
||||
.totals-table { break-inside: avoid; page-break-inside: avoid; }
|
||||
.party-card { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Receipt</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Receipt Number</div>
|
||||
<div class="doc-number">{{RECEIPT_NUMBER}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Receipt Date:</span> <span class="meta-value">{{RECEIPT_DATE}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Issued By</div>
|
||||
<div class="party-name">{{ISSUER_NAME}}</div>
|
||||
<div class="party-detail">{{ISSUER_ADDRESS}}</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Received From</div>
|
||||
<div class="party-name">{{RECEIVED_FROM_NAME}}</div>
|
||||
<div class="party-detail">{{RECEIVED_FROM_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="section-heading">Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22pt">#</th>
|
||||
<th class="desc-col">Description</th>
|
||||
<th class="num" style="width:32pt">Qty</th>
|
||||
<th style="width:32pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{LINEITEM_ROWS}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>{{SUBTOTAL_TEXT}}</td></tr>
|
||||
<tr><td>Tax:</td><td>{{TAX_TEXT}}</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>{{TOTAL_TEXT}}</td></tr>
|
||||
<tr class="received-row"><td>Amount Received:</td><td>{{AMOUNT_RECEIVED_TEXT}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Payment Details -->
|
||||
<div class="section-heading">Payment Details</div>
|
||||
<div class="payment-grid">
|
||||
<span class="payment-label">Payment Method:</span> <span class="payment-value">{{PAYMENT_METHOD_TEXT}}</span>
|
||||
<span class="payment-label">Payment Reference:</span> <span class="payment-value">{{PAYMENT_REFERENCE_TEXT}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">{{NOTES_TEXT}}</div>
|
||||
|
||||
<!-- Issued By -->
|
||||
<div class="section-heading">Issued By</div>
|
||||
<div class="issued-by-grid">
|
||||
<span class="meta-label">Name:</span> <span class="meta-value">{{ISSUED_BY_NAME}}</span>
|
||||
<span class="meta-label">Title:</span> <span class="meta-value">{{ISSUED_BY_TITLE}}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,161 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Receipt</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.doc-number-area { text-align: right; flex-shrink: 0; }
|
||||
.doc-number-label { font-size: 7.5pt; color: var(--theme-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2pt; }
|
||||
.doc-number { font-size: 11pt; font-weight: 700; color: var(--theme-primary); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-card { border: 0.5pt solid var(--theme-border); border-radius: 3pt; padding: 6pt 8pt; }
|
||||
.party-card-header { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 4pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 6pt; font-size: 8.5pt; }
|
||||
.items-table thead tr { background: var(--theme-primary); color: #fff; }
|
||||
.items-table thead th { padding: 5pt 6pt; text-align: left; font-weight: 600; white-space: nowrap; }
|
||||
.items-table thead th.num { text-align: right; }
|
||||
.items-table tbody tr:nth-child(even) { background: var(--theme-surface); }
|
||||
.items-table tbody td { padding: 4pt 6pt; border-bottom: 0.3pt solid var(--theme-border); vertical-align: top; }
|
||||
.items-table tbody td.num { text-align: right; white-space: nowrap; }
|
||||
.totals-wrapper { display: flex; justify-content: flex-end; margin-bottom: 10pt; }
|
||||
.totals-table { border-collapse: collapse; font-size: 8.5pt; min-width: 150pt; }
|
||||
.totals-table td { padding: 2pt 6pt; }
|
||||
.totals-table td:first-child { color: var(--theme-text-muted); }
|
||||
.totals-table td:last-child { text-align: right; }
|
||||
.totals-table .total-row td { font-weight: 700; font-size: 10pt; border-top: 1pt solid var(--theme-primary); padding-top: 4pt; color: var(--theme-heading); }
|
||||
.totals-table .received-row td { font-weight: 700; font-size: 10pt; color: var(--theme-accent); }
|
||||
.payment-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 3pt; margin-bottom: 8pt; }
|
||||
.payment-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.payment-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.issued-by-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 3pt; margin-bottom: 8pt; }
|
||||
.text-section { font-size: 8.5pt; line-height: 1.5; margin-bottom: 4pt; white-space: pre-line; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Receipt</div>
|
||||
<div class="doc-subtitle">Blackthorn Digital Ltd</div>
|
||||
</div>
|
||||
<div class="doc-number-area">
|
||||
<div class="doc-number-label">Receipt Number</div>
|
||||
<div class="doc-number">REC-2026-0083</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Receipt Date:</span> <span class="meta-value">19 February 2026</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Issued By</div>
|
||||
<div class="party-name">Blackthorn Digital Ltd</div>
|
||||
<div class="party-detail">14 Farringdon Street, London, EC4A 4HH, UK</div>
|
||||
<div class="party-detail">VAT No: GB987654321</div>
|
||||
</div>
|
||||
<div class="party-card">
|
||||
<div class="party-card-header">Received From</div>
|
||||
<div class="party-name">Meridian Solutions plc</div>
|
||||
<div class="party-detail">30 King Street, Birmingham, B1 2LT, UK</div>
|
||||
<div class="party-detail">Accounts Payable</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">Items</div>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22pt">#</th>
|
||||
<th style="width:40%">Description</th>
|
||||
<th class="num" style="width:32pt">Qty</th>
|
||||
<th style="width:32pt">Unit</th>
|
||||
<th class="num" style="width:55pt">Unit Price</th>
|
||||
<th class="num" style="width:55pt">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Software licence — Professional tier (annual)</td>
|
||||
<td class="num">5</td>
|
||||
<td>seat</td>
|
||||
<td class="num">£480.00</td>
|
||||
<td class="num">£2,400.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Onboarding & implementation support</td>
|
||||
<td class="num">8</td>
|
||||
<td>hour</td>
|
||||
<td class="num">£150.00</td>
|
||||
<td class="num">£1,200.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Priority support package (12 months)</td>
|
||||
<td class="num">1</td>
|
||||
<td>package</td>
|
||||
<td class="num">£600.00</td>
|
||||
<td class="num">£600.00</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="totals-wrapper">
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr><td>Subtotal:</td><td>£4,200.00</td></tr>
|
||||
<tr><td>VAT (20%):</td><td>£840.00</td></tr>
|
||||
<tr class="total-row"><td>Total:</td><td>£5,040.00</td></tr>
|
||||
<tr class="received-row"><td>Amount Received:</td><td>£5,040.00</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section-heading">Payment Details</div>
|
||||
<div class="payment-grid">
|
||||
<span class="payment-label">Payment Method:</span> <span class="payment-value">BACS Bank Transfer</span>
|
||||
<span class="payment-label">Payment Reference:</span> <span class="payment-value">REC-2026-0083 / PO-MER-4421</span>
|
||||
</div>
|
||||
<div class="section-heading">Notes</div>
|
||||
<div class="text-section">This receipt confirms full payment received for the above services. Please retain for your records. A duplicate copy has been sent to accounts@meridiansolutions.co.uk.</div>
|
||||
<div class="section-heading">Issued By</div>
|
||||
<div class="issued-by-grid">
|
||||
<span class="meta-label">Name:</span> <span class="meta-value">Oliver Pemberton</span>
|
||||
<span class="meta-label">Title:</span> <span class="meta-value">Finance Manager</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,265 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{FULL_NAME}} — Resume</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 15mm 18mm 15mm 18mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 9.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 174mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Header / Name block ── */
|
||||
.resume-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12pt;
|
||||
border-bottom: 2pt solid var(--theme-primary, #1e3a5f);
|
||||
padding-bottom: 8pt;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
.resume-header .company-logo { flex-shrink: 0; margin-top: 2pt; }
|
||||
|
||||
.resume-header-text { flex: 1; }
|
||||
|
||||
.name {
|
||||
font-size: 22pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
font-size: 11pt;
|
||||
font-weight: 400;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-top: 2pt;
|
||||
margin-bottom: 6pt;
|
||||
}
|
||||
|
||||
.contact-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12pt;
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3pt;
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 0.75pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 5pt;
|
||||
}
|
||||
|
||||
/* ── Summary ── */
|
||||
.summary-text {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Experience / Education entries ── */
|
||||
.entry {
|
||||
margin-bottom: 8pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 1pt;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.entry-dates {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entry-org {
|
||||
font-size: 9pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.entry-location {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.entry-bullets {
|
||||
margin-top: 3pt;
|
||||
padding-left: 12pt;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.entry-bullets li {
|
||||
font-size: 8.5pt;
|
||||
margin-bottom: 1.5pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.entry-description {
|
||||
font-size: 8.5pt;
|
||||
line-height: 1.5;
|
||||
margin-top: 2pt;
|
||||
}
|
||||
|
||||
/* ── Skills ── */
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4pt 10pt;
|
||||
}
|
||||
|
||||
.skill-category {
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
.skill-category-name {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.skill-list {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Certifications / Projects ── */
|
||||
.cert-item {
|
||||
font-size: 8.5pt;
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.cert-name {
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
.cert-issuer {
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.entry { break-inside: avoid; page-break-inside: avoid; }
|
||||
.skill-category { break-inside: avoid; page-break-inside: avoid; }
|
||||
.cert-item { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
.entry { page-break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="resume-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="resume-header-text">
|
||||
<div class="name">{{FULL_NAME}}</div>
|
||||
<div class="job-title">{{JOB_TITLE}}</div>
|
||||
<div class="contact-row">
|
||||
<span class="contact-item">{{EMAIL}}</span>
|
||||
<span class="contact-item">{{PHONE}}</span>
|
||||
<span class="contact-item">{{LOCATION}}</span>
|
||||
<span class="contact-item">{{LINKEDIN}}</span>
|
||||
<span class="contact-item">{{WEBSITE}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Summary -->
|
||||
<section>
|
||||
<div class="section-heading">Professional Summary</div>
|
||||
<div class="summary-text">{{SUMMARY}}</div>
|
||||
</section>
|
||||
|
||||
<!-- Experience -->
|
||||
<section>
|
||||
<div class="section-heading">Work Experience</div>
|
||||
{{EXPERIENCE_ITEMS}}
|
||||
</section>
|
||||
|
||||
<!-- Education -->
|
||||
<section>
|
||||
<div class="section-heading">Education</div>
|
||||
{{EDUCATION_ITEMS}}
|
||||
</section>
|
||||
|
||||
<!-- Skills -->
|
||||
<section>
|
||||
<div class="section-heading">Skills</div>
|
||||
<div class="skills-grid">
|
||||
{{SKILLS_ITEMS}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Certifications (optional) -->
|
||||
<section>
|
||||
<div class="section-heading">Certifications</div>
|
||||
{{CERTIFICATIONS_ITEMS}}
|
||||
</section>
|
||||
|
||||
<!-- Projects (optional) -->
|
||||
<section>
|
||||
<div class="section-heading">Projects</div>
|
||||
{{PROJECTS_ITEMS}}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,159 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Alexandra Chen — Resume</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
@page { size: A4; margin: 15mm 18mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 9.5pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.45; }
|
||||
.document { max-width: 174mm; margin: 0 auto; }
|
||||
.resume-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12pt;
|
||||
border-bottom: 2pt solid var(--theme-primary);
|
||||
padding-bottom: 8pt;
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
.resume-header-text { flex: 1; }
|
||||
.name { font-size: 22pt; font-weight: 700; color: var(--theme-heading); letter-spacing: -0.5px; line-height: 1.1; }
|
||||
.job-title { font-size: 11pt; font-weight: 400; color: var(--theme-accent); margin-top: 2pt; margin-bottom: 6pt; }
|
||||
.contact-row { display: flex; flex-wrap: wrap; gap: 12pt; font-size: 8.5pt; color: var(--theme-text-muted); }
|
||||
.section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 1px; border-bottom: 0.75pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 5pt; }
|
||||
.summary-text { font-size: 9pt; line-height: 1.5; }
|
||||
.entry { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.entry-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 1pt; }
|
||||
.entry-title { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); }
|
||||
.entry-dates { font-size: 8pt; color: var(--theme-text-muted); white-space: nowrap; }
|
||||
.entry-org { font-size: 9pt; font-weight: 600; color: var(--theme-accent); margin-bottom: 2pt; }
|
||||
.entry-bullets { margin-top: 3pt; padding-left: 12pt; list-style: disc; }
|
||||
.entry-bullets li { font-size: 8.5pt; margin-bottom: 1.5pt; line-height: 1.4; }
|
||||
.skills-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4pt 10pt; }
|
||||
.skill-category-name { font-size: 8.5pt; font-weight: 700; color: var(--theme-primary); margin-bottom: 2pt; }
|
||||
.skill-list { font-size: 8pt; line-height: 1.5; }
|
||||
.cert-item { font-size: 8.5pt; margin-bottom: 3pt; }
|
||||
.cert-name { font-weight: 600; }
|
||||
.cert-issuer { color: var(--theme-text-muted); font-size: 8pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<header class="resume-header">
|
||||
<div class="resume-header-text">
|
||||
<div class="name">Alexandra Chen</div>
|
||||
<div class="job-title">Senior Software Engineer</div>
|
||||
<div class="contact-row">
|
||||
<span>alex.chen@example.com</span>
|
||||
<span>+1 (555) 234-5678</span>
|
||||
<span>San Francisco, CA</span>
|
||||
<span>linkedin.com/in/alexchen</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div class="section-heading">Professional Summary</div>
|
||||
<div class="summary-text">Results-driven Senior Software Engineer with 8+ years of experience designing and building scalable distributed systems. Proven track record of leading cross-functional teams, reducing infrastructure costs by 40%, and shipping products used by millions of users. Passionate about developer experience, performance optimisation, and mentoring junior engineers.</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-heading">Work Experience</div>
|
||||
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-title">Senior Software Engineer</span>
|
||||
<span class="entry-dates">Mar 2021 – Present</span>
|
||||
</div>
|
||||
<div class="entry-org">TechCorp Inc. — San Francisco, CA</div>
|
||||
<ul class="entry-bullets">
|
||||
<li>Led architecture redesign of core payments service, reducing p99 latency from 800ms to 120ms.</li>
|
||||
<li>Managed a team of 6 engineers across 3 time zones, shipping 2 major product releases per year.</li>
|
||||
<li>Reduced cloud infrastructure spend by 40% through rightsizing and caching strategies.</li>
|
||||
<li>Established engineering standards and code review practices adopted company-wide.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-title">Software Engineer II</span>
|
||||
<span class="entry-dates">Jun 2018 – Mar 2021</span>
|
||||
</div>
|
||||
<div class="entry-org">StartupXYZ — New York, NY</div>
|
||||
<ul class="entry-bullets">
|
||||
<li>Built real-time data pipeline processing 50M events/day using Kafka and Apache Flink.</li>
|
||||
<li>Designed and launched REST API serving 5M daily active users with 99.95% uptime.</li>
|
||||
<li>Reduced CI/CD pipeline duration from 45 min to 8 min through parallelisation.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-title">Software Engineer</span>
|
||||
<span class="entry-dates">Aug 2016 – Jun 2018</span>
|
||||
</div>
|
||||
<div class="entry-org">DataSolutions Ltd — Boston, MA</div>
|
||||
<ul class="entry-bullets">
|
||||
<li>Developed ETL pipelines for Fortune 500 clients, processing terabytes of data nightly.</li>
|
||||
<li>Built internal tooling that saved the analytics team 10 hours per week.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-heading">Education</div>
|
||||
<div class="entry">
|
||||
<div class="entry-header">
|
||||
<span class="entry-title">B.Sc. Computer Science</span>
|
||||
<span class="entry-dates">2012 – 2016</span>
|
||||
</div>
|
||||
<div class="entry-org">Massachusetts Institute of Technology (MIT)</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-heading">Skills</div>
|
||||
<div class="skills-grid">
|
||||
<div>
|
||||
<div class="skill-category-name">Languages</div>
|
||||
<div class="skill-list">Python, Go, TypeScript, Java, SQL</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="skill-category-name">Infrastructure</div>
|
||||
<div class="skill-list">AWS, GCP, Kubernetes, Terraform, Docker</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="skill-category-name">Frameworks & Tools</div>
|
||||
<div class="skill-list">React, Node.js, FastAPI, Kafka, Redis, PostgreSQL</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-heading">Certifications</div>
|
||||
<div class="cert-item">
|
||||
<span class="cert-name">AWS Certified Solutions Architect – Professional</span>
|
||||
<span class="cert-issuer"> · Amazon Web Services · 2023</span>
|
||||
</div>
|
||||
<div class="cert-item">
|
||||
<span class="cert-name">Google Cloud Professional Data Engineer</span>
|
||||
<span class="cert-issuer"> · Google · 2022</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,323 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SAFE Agreement</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
/* ── Divider ── */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
margin: 6pt 0;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 2pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 8.5pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Parties ── */
|
||||
.parties-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 2pt;
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
font-size: 8pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── Key financial terms highlight box ── */
|
||||
.terms-box {
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-left: 3pt solid var(--theme-accent, #2563eb);
|
||||
padding: 8pt 10pt;
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.terms-box-title {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 6pt;
|
||||
}
|
||||
|
||||
.terms-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 120pt auto;
|
||||
row-gap: 3pt;
|
||||
}
|
||||
|
||||
.terms-kv-label {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.terms-kv-value {
|
||||
font-size: 8.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
/* ── Legal sections ── */
|
||||
.legal-section {
|
||||
margin-bottom: 8pt;
|
||||
}
|
||||
|
||||
.legal-section-heading {
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-heading, #111827);
|
||||
margin-bottom: 3pt;
|
||||
}
|
||||
|
||||
.legal-section-body {
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 16pt;
|
||||
}
|
||||
|
||||
.sig-col {
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.legal-section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.sig-col { break-inside: avoid; page-break-inside: avoid; }
|
||||
.parties-grid { break-inside: avoid; page-break-inside: avoid; }
|
||||
.terms-box { break-inside: avoid; page-break-inside: avoid; }
|
||||
.terms-kv { break-inside: avoid; page-break-inside: avoid; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">{{AGREEMENT_TITLE}}</div>
|
||||
<div class="doc-subtitle">Simple Agreement for Future Equity</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">{{AGREEMENT_REF}}</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">{{GOVERNING_LAW}}</span>
|
||||
<span class="meta-label">Classification:</span> <span class="meta-value">{{CONFIDENTIALITY_LABEL}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Parties -->
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Company</div>
|
||||
<div class="party-name">{{COMPANY_LEGAL_NAME}}</div>
|
||||
<div class="party-detail">{{COMPANY_ADDRESS}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Investor</div>
|
||||
<div class="party-name">{{INVESTOR_LEGAL_NAME}}</div>
|
||||
<div class="party-detail">{{INVESTOR_ADDRESS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key financial terms -->
|
||||
<div class="terms-box">
|
||||
<div class="terms-box-title">Key Financial Terms</div>
|
||||
<div class="terms-kv">
|
||||
<span class="terms-kv-label">Purchase Amount:</span> <span class="terms-kv-value">{{PURCHASE_AMOUNT}}</span>
|
||||
<span class="terms-kv-label">Valuation Cap:</span> <span class="terms-kv-value">{{VALUATION_CAP}}</span>
|
||||
<span class="terms-kv-label">Discount Rate:</span> <span class="terms-kv-value">{{DISCOUNT_RATE}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal sections -->
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Purchase Amount</div>
|
||||
<div class="legal-section-body">In consideration of the rights described in this Agreement, the Investor agrees to invest {{PURCHASE_AMOUNT}} in the Company (the "Purchase Amount"). The Company shall use the Purchase Amount for general corporate and operating purposes.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Valuation Cap</div>
|
||||
<div class="legal-section-body">This SAFE is subject to a valuation cap of {{VALUATION_CAP}} (the "Valuation Cap"). The Valuation Cap is used to determine the number of shares issued to the Investor upon conversion of this SAFE.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Discount Rate</div>
|
||||
<div class="legal-section-body">This SAFE includes a discount rate of {{DISCOUNT_RATE}} applicable upon conversion. This discount shall be applied to the price per share paid by investors in the relevant equity financing round, and the Investor shall receive shares at the lower of the discounted price or the Valuation Cap price.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Most Favoured Nation</div>
|
||||
<div class="legal-section-body">{{MFN_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Conversion Events</div>
|
||||
<div class="legal-section-body">{{CONVERSION_EVENTS_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Termination</div>
|
||||
<div class="legal-section-body">{{TERMINATION_TEXT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Governing Law</div>
|
||||
<div class="legal-section-body">This Agreement shall be governed by and construed in accordance with the laws of {{GOVERNING_LAW}}.</div>
|
||||
</div>
|
||||
|
||||
<!-- Signatures -->
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">IN WITNESS WHEREOF, the parties have executed this Agreement as of the date first written above.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Company</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: {{COMPANY_SIGNATORY}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{SIGNATURE_DATE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Investor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: {{INVESTOR_SIGNATORY}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{SIGNATURE_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,166 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SAFE Agreement — Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { text-align: center; margin-bottom: 8pt; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
hr { border: none; border-top: 0.5pt solid var(--theme-border); margin: 6pt 0; }
|
||||
.meta-grid { display: grid; grid-template-columns: 120pt auto; row-gap: 2pt; margin-bottom: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 8.5pt; }
|
||||
.meta-value { font-size: 8.5pt; color: var(--theme-text); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.parties-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10pt; margin-bottom: 8pt; }
|
||||
.party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 3pt; }
|
||||
.party-name { font-size: 9.5pt; font-weight: 600; color: var(--theme-heading); margin-bottom: 2pt; }
|
||||
.party-detail { font-size: 8pt; color: var(--theme-text-muted); line-height: 1.5; }
|
||||
.terms-box { background: var(--theme-surface); border: 0.5pt solid var(--theme-border); border-left: 3pt solid var(--theme-accent); padding: 8pt 10pt; margin-bottom: 8pt; }
|
||||
.terms-box-title { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 6pt; }
|
||||
.terms-kv { display: grid; grid-template-columns: 120pt auto; row-gap: 3pt; }
|
||||
.terms-kv-label { font-size: 8.5pt; font-weight: 600; color: var(--theme-text-muted); }
|
||||
.terms-kv-value { font-size: 8.5pt; font-weight: 700; color: var(--theme-heading); }
|
||||
.legal-section { margin-bottom: 8pt; page-break-inside: avoid; }
|
||||
.legal-section-heading { font-size: 9.5pt; font-weight: 700; color: var(--theme-heading); margin-bottom: 3pt; }
|
||||
.legal-section-body { font-size: 9pt; line-height: 1.6; color: var(--theme-text); }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 16pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } .legal-section { page-break-inside: avoid; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<div class="doc-header">
|
||||
<div class="doc-title">SAFE Agreement</div>
|
||||
<div class="doc-subtitle">Simple Agreement for Future Equity</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Reference:</span> <span class="meta-value">SAFE-2026-003</span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">15 February 2026</span>
|
||||
<span class="meta-label">Governing Law:</span> <span class="meta-value">England and Wales</span>
|
||||
<span class="meta-label">Classification:</span> <span class="meta-value">Confidential</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Parties</div>
|
||||
<div class="parties-grid">
|
||||
<div>
|
||||
<div class="party-label">Company</div>
|
||||
<div class="party-name">Helix Biotech Ltd</div>
|
||||
<div class="party-detail">72 Science Park Road, Cambridge, CB4 0DW, United Kingdom</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="party-label">Investor</div>
|
||||
<div class="party-name">Beacon Ventures Fund II LP</div>
|
||||
<div class="party-detail">8 St James's Square, London, SW1Y 4JU, United Kingdom</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms-box">
|
||||
<div class="terms-box-title">Key Financial Terms</div>
|
||||
<div class="terms-kv">
|
||||
<span class="terms-kv-label">Purchase Amount:</span> <span class="terms-kv-value">£500,000</span>
|
||||
<span class="terms-kv-label">Valuation Cap:</span> <span class="terms-kv-value">£8,000,000 post-money</span>
|
||||
<span class="terms-kv-label">Discount Rate:</span> <span class="terms-kv-value">20%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Agreement</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">1. Purchase Amount</div>
|
||||
<div class="legal-section-body">In consideration of the rights described in this Agreement, the Investor agrees to invest £500,000 in the Company (the "Purchase Amount"). The Company shall use the Purchase Amount for general corporate and operating purposes, including research and development activities.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">2. Valuation Cap</div>
|
||||
<div class="legal-section-body">This SAFE is subject to a valuation cap of £8,000,000 post-money (the "Valuation Cap"). The Valuation Cap is used to determine the number of shares issued to the Investor upon conversion of this SAFE. The Investor will receive shares based on whichever calculation results in a lower price per share: the Valuation Cap price or the discounted price.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">3. Discount Rate</div>
|
||||
<div class="legal-section-body">This SAFE includes a discount rate of 20% applicable upon conversion. This discount shall be applied to the price per share paid by new investors in the relevant equity financing round. The Investor shall receive shares at the lower of: (a) the price per share multiplied by 80%; or (b) the price per share calculated using the Valuation Cap.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">4. Most Favoured Nation</div>
|
||||
<div class="legal-section-body">If the Company issues any SAFEs or convertible instruments to future investors on terms more favourable than those set out herein (including a lower valuation cap or higher discount rate), the Company shall promptly notify the Investor and the Investor shall have the right, within thirty (30) days of such notification, to elect to have this SAFE amended to incorporate such more favourable terms.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">5. Conversion Events</div>
|
||||
<div class="legal-section-body">This SAFE will automatically convert into shares of the Company's preferred stock (or equivalent) upon the occurrence of any of the following events ("Conversion Events"):
|
||||
|
||||
(a) Equity Financing: An equity financing in which the Company sells preferred shares for aggregate gross proceeds of at least £2,000,000, at which time this SAFE converts into shares of the same class at the conversion price determined under Sections 2 and 3 above.
|
||||
|
||||
(b) Liquidity Event: A change of control, sale, merger, or similar transaction, at which time the Investor shall receive, at the Investor's election, either: (i) a cash payment equal to the Purchase Amount; or (ii) shares of the Company immediately prior to the liquidity event at the conversion price.
|
||||
|
||||
(c) Dissolution Event: In the event of a winding-up or dissolution of the Company, the Investor shall be entitled to receive, prior to any distribution to ordinary shareholders, an amount equal to the Purchase Amount, subject to the rights of any senior creditors.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">6. Termination</div>
|
||||
<div class="legal-section-body">This SAFE will terminate (without any payment to the Investor) upon: (a) conversion in full pursuant to Section 5; or (b) payment in full of the Purchase Amount in the case of a dissolution event or liquidity event as described in Section 5. No party may unilaterally terminate this SAFE prior to a Conversion Event.</div>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<div class="legal-section-heading">7. Governing Law</div>
|
||||
<div class="legal-section-body">This Agreement shall be governed by and construed in accordance with the laws of England and Wales. The parties irrevocably submit to the exclusive jurisdiction of the courts of England and Wales to resolve any dispute arising under or in connection with this Agreement.</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Signatures</div>
|
||||
<div class="legal-section-body">IN WITNESS WHEREOF, the parties have executed this Agreement as of the date first written above.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Company</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: Dr. Amelia Hargreaves, CEO</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 15 February 2026</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">For the Investor</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name / Title: Thomas Beacon, General Partner</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 15 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,158 +0,0 @@
|
||||
# Sample Prompts
|
||||
|
||||
One sample prompt per document template. Use these to test generation or as inspiration for users.
|
||||
|
||||
---
|
||||
|
||||
## advisor_agreement
|
||||
Create an advisor agreement between Helix Biotech Ltd (Company Registration Number 09876543), whose registered office is at 42 Innovation Way, Cambridge CB2 8AB, and Dr. Priya Sharma of 15 Oak Lane, London NW3 2PT, a clinical research advisor. The agreement is effective from 1 April 2026. Dr. Sharma will advise on regulatory strategy (including MHRA and EMA submissions), clinical trial design, and protocol development for the Company's lead oncology programme (Project代号 HELIX-ONC-001) for an initial term of 18 months, extendable by mutual written agreement. She commits to a minimum of 4 hours per week, with availability for up to 2 board or advisory committee meetings per quarter. Compensation: 0.3% equity options (fully diluted) vesting monthly over 24 months with a 6-month cliff from the effective date, plus a monthly retainer of £500 payable in arrears by the 15th of each month. She will receive reimbursement of reasonable out-of-pocket expenses incurred with prior approval. She confirms she has no conflicting commitments and will maintain confidentiality of all Company information. The agreement includes standard indemnification for advice given in good faith, and termination on 30 days' written notice by either party, with any unvested options lapsing on termination. Intellectual property arising from the advisory work is assigned to the Company. The agreement is governed by English law and subject to the exclusive jurisdiction of the courts of England and Wales. Include signature blocks for both parties and the date of execution.
|
||||
|
||||
---
|
||||
|
||||
## audit_report
|
||||
Create an internal audit report for Vantage Capital Partners Ltd. Audit reference IA-2026-11. The audit was conducted over the period 15 January to 31 March 2026 and covered payroll processing and HR data controls across the firm's London and Edinburgh offices. The audit was performed by the internal audit function; lead auditor James Hartley (CIA, CISA), with fieldwork support from Emma Chen and Oliver Wright. The scope included: payroll authorisation and segregation of duties, access rights to the HR system (PeopleHub) and payroll system (Sage Payroll), leaver process and access revocation, overtime and exception payment approvals, and completeness of personnel file documentation. Findings: (1) High risk — payroll system access was not revoked for 12 former employees within the required 24-hour window; the longest outstanding revocation was 47 days for a leaver from the Edinburgh office; management has since revoked all 12 accounts and implemented a weekly leaver-access report. (2) Medium risk — the overtime approval process was bypassed in 3 instances in February 2026, with overtime paid on manager verbal approval without documented sign-off; control has been reinforced with mandatory workflow in the system. (3) Low risk — annual salary review documentation was incomplete for 8 staff (missing signed forms or calibration notes); HR has committed to completing files by 30 April 2026. Overall opinion: Partially Satisfactory. Include an executive summary, detailed findings with criteria and recommendations, management response for each finding, and sign-off from the Head of Internal Audit. Report date: 10 April 2026.
|
||||
|
||||
---
|
||||
|
||||
## board_minutes
|
||||
Create board minutes for Hartwell Group plc. Meeting of the Board of Directors held on 15 April 2026 at 9:00am at the Company's offices at 100 Fleet Street, London EC4Y 1DE. Directors present: Sir Michael Hartwell (Chair), Catherine Ashworth, David Okonkwo, Fiona Reid, James Park. In attendance: Rachel Dunmore (incoming CFO, for item 2), company secretary Patricia Lowe. Apologies: none. The Chair confirmed a quorum and opened the meeting. (1) Minutes of the previous meeting (18 March 2026) were approved as a true record. (2) Q1 2026 financial results: The CFO (outgoing) presented the unaudited Q1 figures. Revenue £18.2M (up 8% YoY), EBITDA £4.2M (up 12% YoY), net debt £6.1M. The Board discussed the performance and noted strength in the UK and Nordic regions. Resolution: to approve the Q1 2026 financial results and their release to the market. Passed unanimously. (3) Appointment of CFO: The Chair proposed the appointment of Rachel Dunmore as Chief Financial Officer with effect from 1 May 2026, on terms set out in the board paper. One director abstained citing a prior professional relationship; the resolution was passed 4–1. (4) Acquisition pipeline: The CEO presented three potential acquisition targets (Meridian Analytics Ltd, and two others in confidence). The Board agreed to approve proceeding to due diligence on Meridian Analytics Ltd only, with a cap of £150K on diligence costs, and to receive an update at the May board. (5) Any other business: none. Next meeting: 20 May 2026. The meeting closed at 10:45am. Include standard wording for distribution and signing.
|
||||
|
||||
---
|
||||
|
||||
## budget_proposal
|
||||
Create a budget proposal for Crestwood Technologies Ltd for financial year 2026/27 (1 April 2026 to 31 March 2027). Purpose: to seek Board approval for the operating budget. Total proposed budget: £3.4M. Revenue projections: SaaS subscriptions £2.1M (assuming 18% ARR growth and two new enterprise customers signed in Q1), professional services £800K, support and maintenance contracts £500K. Major expense lines: engineering and product headcount £1.2M (current team of 12, no new hires in budget), cloud infrastructure and tooling £340K, sales and marketing £620K (including one new SDR and conference spend), G&A £480K (finance, legal, office), R&D and innovation £360K. Contingency: £100K unallocated. Key assumptions: 18% ARR growth, churn not to exceed 5% annualised, professional services margin held at 35%. Risks: delayed enterprise deal could reduce revenue by £200K; mitigation is pipeline acceleration and cost holdbacks after Q2 review. Include a one-page executive summary, revenue and cost breakdown tables, and a request for approval with date and authority. Document owner: Finance Director. Version 1.2, dated 12 March 2026.
|
||||
|
||||
---
|
||||
|
||||
## case_study
|
||||
Create a case study (2–3 pages) for Stirling PDF. Client: Nexus Cloud Ltd, a mid-market financial services firm based in London with 280 employees and operations in the UK and Ireland. Their challenge: manual document processing for client onboarding, KYC packs, and regulatory filings was consuming approximately 3 FTE and averaging 4 days per document, with a high error rate and poor visibility for compliance. They piloted Stirling PDF's AI document generation platform in Q3 2025 and rolled out to the operations and legal teams in Q4 2025. Solution: integrated Stirling PDF with their existing CRM and document store; used AI-assisted templates for standard document types; introduced approval workflows and audit trails. Results after 6 months: average processing time reduced from 4 days to 45 minutes for standard documents; the 3 FTE were redeployed to exception handling and client service; compliance error rate in sampled audits dropped by 94%; time to onboard a new corporate client reduced from 12 days to 4 days. Include a short quote/testimonial from their CTO, Marcus Reid: "Stirling PDF didn't just speed things up — it gave us control and traceability we never had with spreadsheets and email." Add a "Why Nexus Cloud chose Stirling PDF" section and a call-to-action for readers. Format for use on the Stirling PDF website and sales collateral.
|
||||
|
||||
---
|
||||
|
||||
## committee_agenda
|
||||
Create an agenda for the Audit and Risk Committee of Meridian Healthcare NHS Foundation Trust. Meeting: 26 March 2026, 10:00am–12:00pm, Trust Headquarters, Bristol (Room 4.2, and via MS Teams for remote attendees). Chair: Non-Executive Director Dr. Anne Whitfield. Attendees: all Audit and Risk Committee members (as per standing list), Director of Finance (Sarah Okonkwo), Deputy Director of Finance, Head of Internal Audit, and representatives from external auditors Grant Thornton (lead partner James Hartley, manager Emma Chen). Apologies to be notified to the Committee Secretary. Agenda: (1) Welcome and declarations of interest (5 mins). (2) Minutes of the previous meeting (14 January 2026) — for approval (5 mins). (3) External audit progress update — Grant Thornton to report on 2025/26 audit progress, key risks, and timetable (20 mins). (4) Internal audit — Q4 findings and annual report summary; Head of Internal Audit to present (25 mins). (5) Risk register review — updated strategic and operational risks; discussion of any new or escalated risks (20 mins). (6) Financial controls update — Director of Finance to update on control improvements and any incidents (15 mins). (7) Any other business (5 mins). (8) Date of next meeting: 21 May 2026. Papers circulated with the agenda: Q4 internal audit report, updated risk register (version 3.2), management accounts to February 2026, external audit progress report. Contact: Committee Secretary, ext. 4521.
|
||||
|
||||
---
|
||||
|
||||
## employee_handbook
|
||||
Create an employee handbook for Northgate Engineering Services Ltd, effective 1 January 2026. The company is an engineering consultancy with offices in Birmingham and Leeds; the handbook applies to all UK employees. Include: (1) Welcome and introduction from the Managing Director; company values and mission. (2) Working hours: standard 37.5 hours per week; flexible start between 7:30am and 9:30am; core hours 10am–3pm; break entitlements. (3) Annual leave: 25 days plus 8 bank holidays, rising to 28 days after 5 years' service; carry-over of up to 5 days with manager approval; booking procedure. (4) Sick leave: self-certification for up to 7 days; company sick pay after 3 months' service for up to 3 months full pay, then 3 months half pay; notification and evidence requirements. (5) Code of conduct: integrity, respect, diversity and inclusion, anti-bribery, conflicts of interest, use of social media. (6) Health and safety: responsibilities of the company and employees; reporting accidents and near misses; DSE and risk assessments. (7) IT and data security: acceptable use, passwords, data protection (UK GDPR), confidential information, bring-your-own-device policy. (8) Disciplinary and grievance procedures: informal and formal stages; right to be accompanied; appeals. (9) Other: probation (6 months), expenses, training and development, equal opportunities, maternity/paternity and other family leave (summary). State that the handbook is not contractual and may be updated; governed by UK employment law. Include a sign-off page for employees to acknowledge receipt.
|
||||
|
||||
---
|
||||
|
||||
## executive_summary
|
||||
Create an executive summary (2–3 pages) for the proposed acquisition of Trident Manufacturing Group Ltd by Aldgate Capital Partners LLP. Deal: Aldgate proposes to acquire 100% of the equity of Trident for £28M (enterprise value), with £3M of cash and £25M of assumed debt and seller rollover. Trident: UK-based precision engineering and manufacturing group; revenue £12M (last 12 months), EBITDA £2.1M, 3 manufacturing sites in the Midlands (Birmingham, Coventry, Wolverhampton), 180 employees. Key findings from commercial and financial due diligence: (1) Strong order book covering 14 months of production; key customers in aerospace and defence. (2) Aging plant and machinery; capex of approximately £1.8M required in year 2 for replacement and compliance. (3) Two key customer contracts (c. 30% of revenue) up for renewal in Q3 2026; renewal risk is material. (4) Management team is strong; retention packages recommended. (5) No material environmental or litigation issues identified. Recommendation: proceed with the acquisition subject to (a) capex allowance of £1.8M reflected in price or vendor warranty, and (b) earnout or deferred consideration linked to renewal of the two key contracts. Indicative timetable: exclusivity to 30 April, completion by end June 2026. Prepared by Aldgate deal team. Confidential. Date: 8 April 2026.
|
||||
|
||||
---
|
||||
|
||||
## expense_report
|
||||
Create an expense report for James Okafor, Sales Director at Pinnacle Cloud Services Ltd. Employee ID: EMP-10482. Department: Sales. Period: 1–28 February 2026. Expenses: (1) Client dinner at The Ivy, London on 12 February 2026 — £347.50; purpose: client entertainment with Meridian Solutions (4 attendees: James Okafor, 2 from Meridian, 1 from Pinnacle); receipt attached. (2) Train travel London Euston to Manchester Piccadilly and return on 18 February 2026 — £214.00; purpose: client meeting at Caldwell Retail Group; booking reference attached. (3) Hotel — Premier Inn Manchester Central, 18 February 2026 (one night) — £189.00; receipt attached. (4) Taxi and ground transport — various dates across February (airport transfers and client meetings) — £67.40; summary and receipts attached. Total expenses: £817.90. Currency: GBP. No advances received for this period. Declaration: I confirm that these expenses were incurred in the course of business and comply with the company expense policy. Submit for approval to: Karen Delgado, Director of Operations. Payment: reimbursement to bank account ending 8821 within 10 working days of approval. Include space for approver signature and date.
|
||||
|
||||
---
|
||||
|
||||
## incident_report
|
||||
Create an incident report for Ironbridge Engineering Solutions Ltd. Report reference: INC-2026-031. Date of incident: 6 March 2026. Time: 2:15pm. Location: Workshop Bay 3, Ironbridge Manufacturing Site, Telford. Reported by: Site Supervisor David Chen. Person involved: Thomas Walsh, Warehouse Operative (Employee ID 2047), 3 years' service. Summary: Mr Walsh sustained a back injury while manually lifting a 40kg component (engine housing) in Workshop Bay 3. He felt immediate pain and was unable to continue; first aid was administered on site by trained first aider Fiona Reid; he was then transported to A&E at Princess Royal Hospital, Telford. Diagnosis: lower back strain; he was discharged with advice and signed off work for 2 weeks. The incident is RIDDOR-reportable (non-fatal injury to an employee); the HSE was notified within the required timeframe. Root cause analysis: (1) No mechanical lifting aid (pallet jack or crane) was available in Bay 3; the nearest pallet jack was in Bay 1. (2) Mr Walsh had not received refresher manual handling training since 2023. (3) Risk assessment for Bay 3 had not been updated to reflect the regular movement of 40kg+ items. Corrective actions: (1) Procure and install two additional pallet jacks for Bay 3 by 20 March 2026. (2) Mandatory refresher manual handling training for all warehouse and workshop staff by 30 April 2026. (3) Update risk assessment for Workshop Bay 3 and implement a check before manual lift of items over 25kg. (4) Brief all relevant staff on the incident and lessons learned by 13 March 2026. Report prepared by: Health & Safety Manager. Approved by: Site Director. Date: 10 March 2026.
|
||||
|
||||
---
|
||||
|
||||
## independent_contractor_agreement
|
||||
Create an independent contractor agreement between Vantage Digital Solutions Ltd (Company Number 11223344), registered office 50 Tech Park, Edinburgh EH12 9AB (the "Client"), and Owen Clarke Consulting Ltd (Company Number 55667788), registered office 22 Consultant Lane, Glasgow G1 1AA (the "Contractor"). Commencement: 1 March 2026. Term: 6 months, unless terminated earlier. Services: the Contractor will provide senior backend engineering services (Python, AWS, system design) as directed by the Client's Engineering Manager, including code development, code review, and technical documentation. The Contractor will provide personnel as agreed in statements of work; key resource: Owen Clarke or an agreed substitute. Time commitment: up to 5 days per week; remote working with one day per week on-site at the Client's Edinburgh office (travel expenses reimbursed in line with policy). Day rate: £650 per day plus VAT; invoiced monthly in arrears; payment within 30 days. The Contractor is responsible for its own tax, NI, and insurance. Intellectual property: all IP and work product created in performing the services is assigned to the Client; the Contractor will execute any further documents required. Confidentiality: both parties will keep confidential all non-public information; obligation survives termination for 2 years. Non-solicitation: for 90 days after the end of the engagement, the Contractor will not solicit or hire the Client's employees or contractors. Termination: either party may terminate on 4 weeks' written notice; the Client may terminate for material breach with 14 days' notice to remedy. The agreement is governed by Scots law. Include signature blocks and schedule for statements of work.
|
||||
|
||||
---
|
||||
|
||||
## invoice
|
||||
Create an invoice from Blackthorn Digital Ltd (VAT Registration GB 123 4567 89), 15 Design Quarter, London SE1 2AB, to Meridian Solutions plc, 100 Commerce House, Manchester M2 5AB. Invoice number: INV-2026-047. Invoice date: 28 February 2026. Payment due: 29 March 2026 (30 days). Purchase order reference (if provided by customer): PO-MERI-2026-012. Line items: (1) UX design sprint — 40 hours at £125.00 per hour — £5,000.00; (2) Brand guidelines document — 1 deliverable — £1,200.00; (3) Monthly retainer February 2026 — £2,500.00. Subtotal: £8,700.00. VAT at 20%: £1,740.00. Total due: £10,440.00. Payment by bank transfer: Barclays Bank, sort code 20-00-00, account number 12345678. Reference: INV-2026-047 and your company name. Late payment: interest may be charged in accordance with the Late Payment of Commercial Debts (Interest) Act 1998. Blackthorn Digital Ltd is a company registered in England and Wales, number 09876543. Include standard terms reference (e.g. "Subject to our standard terms of business") and a thank-you message.
|
||||
|
||||
---
|
||||
|
||||
## job_description
|
||||
Create a job description for a Lead Software Engineer at Hartwell Digital Agency Ltd. Location: Manchester (Hybrid — 3 days per week in the office, 2 days remote). Reports to: Head of Engineering. Salary: £75,000–£90,000 per annum depending on experience. Benefits: 25 days holiday plus bank holidays, pension (5% employer contribution), private health insurance, annual training budget £2,000, cycle-to-work scheme. Role purpose: to lead a small team of backend engineers in designing, building, and maintaining scalable services and APIs for client projects and internal products; to contribute to architecture decisions and technical standards. Key responsibilities: lead and mentor a team of 3–5 engineers; own the technical design and delivery of backend features; conduct code reviews and ensure quality and security; work closely with product and design to define requirements and timelines; contribute to hiring and onboarding. Requirements: 5+ years of commercial backend engineering experience; strong proficiency in Python and Go; experience with AWS or GCP (EC2, Lambda, RDS, S3 or equivalent); experience leading or mentoring engineers; comfortable with CI/CD (e.g. GitHub Actions, Jenkins), infrastructure as code (Terraform or similar), and agile delivery. Nice to have: experience in agency or professional services; familiarity with React or front-end integration. Closing date for applications: 13 March 2026. Equal opportunities employer. Include application instructions (CV and short cover letter to careers@hartwelldigital.co.uk, quoting reference LSE-2026-01).
|
||||
|
||||
---
|
||||
|
||||
## letter
|
||||
Write a formal business letter from Sarah Whitfield, Head of Procurement, Pinnacle Retail Group plc, 200 Retail Way, Birmingham B12 8AB (tel. 0121 123 4567, sarah.whitfield@pinnacleretail.co.uk), to the Account Manager, Apex Logistics Ltd, Apex House, 50 Distribution Park, Northampton NN3 8AB. Date: 15 February 2026. Subject: Notice of termination — warehousing contract. The letter should formally notify Apex that Pinnacle is exercising its 90-day termination clause under the current warehousing and distribution contract (contract reference: PRG-APEX-2022-01), with effect from 31 May 2026. The reason for termination is a strategic decision by Pinnacle to bring warehousing operations in-house following the opening of their new distribution centre in Coventry. The letter should thank Apex for the 4-year business relationship and the quality of service provided, and request a transition meeting within the next 14 days to agree handover steps, including inventory reconciliation, return of any Pinnacle assets, and final account settlement. Ask for confirmation of receipt and the name of the person who will lead the transition on Apex's side. Formal tone; sign off "Yours sincerely" with Sarah Whitfield's name and title. Letterhead style (company name and address at top).
|
||||
|
||||
---
|
||||
|
||||
## letter_of_intent
|
||||
Create a letter of intent from Meridian Capital Advisors LLP (address: 75 Capital Square, London EC2V 7AB) to the Board of Directors of Trident Manufacturing Group Ltd (Trident House, Birmingham B6 4AB). Date: 1 April 2026. Subject: Proposed equity investment. Meridian expresses its non-binding interest in investing £3.5M in Trident in return for a 22% equity stake, on a pre-money valuation of £16M. The investment would be used primarily to fund expansion of Trident's Birmingham facility and the implementation of a new ERP system. Indicative timeline: exclusivity period of 30 days from signing of this LOI; due diligence (commercial, financial, legal) to be completed within 45 days; target completion by 30 June 2026. The LOI is non-binding except for: (1) confidentiality — both parties will keep the existence and terms of the LOI and any shared information confidential; (2) exclusivity — Trident will not solicit or entertain other offers for the duration of the exclusivity period; (3) costs — each party bears its own costs. Binding terms would be set out in definitive investment documentation. Governing law: English law. Request that Trident countersign to indicate its willingness to proceed on this basis. Include signature blocks for both parties.
|
||||
|
||||
---
|
||||
|
||||
## master_services_agreement
|
||||
Create a master services agreement between Pinnacle Cloud Services Ltd (Company Number 11223344), registered office 10 Cloud Street, Reading RG1 2AB ("Provider"), and Caldwell Retail Group plc (Company Number 09876543), registered office 5 High Street, Leeds LS1 1AA ("Client"). Services: the Provider will supply (1) cloud infrastructure management (AWS/Azure as agreed per order), (2) 24/7 monitoring and alerting, and (3) monthly security audits and reporting. Services will be specified in order forms or statements of work under this MSA. Fees: £18,000 per month (excluding VAT) for the baseline scope as set out in Order Form #1; additional services at agreed rates. Fees are reviewed annually with effect from each anniversary of the effective date. Term: initial term 2 years from the effective date; thereafter auto-renewing for successive 12-month periods unless either party gives at least 90 days' notice before the end of the then-current term. Data and privacy: the Provider processes personal data on behalf of the Client as a data processor under UK GDPR; a data processing addendum is incorporated. Liability: cap at 12 months of fees paid by the Client in the 12 months preceding the claim; no liability for indirect or consequential loss. Termination: for material breach (30 days to remedy); for convenience after the initial term on 30 days' written notice. Effects of termination: return of data, payment for services to date. Governing law and jurisdiction: English law, exclusive jurisdiction of the courts of England and Wales. Include entire agreement, force majeure, notices, and signature blocks.
|
||||
|
||||
---
|
||||
|
||||
## meeting_minutes
|
||||
Create meeting minutes for the Q1 Product Review meeting of Stirling PDF SaaS. Date: 5 March 2026. Time: 10:00am–11:30am. Location: Zoom (link sent in calendar invite). Chair: Ethan (CEO). Attendees: Ethan (CEO), Rachel (Product), Marcus (Engineering), Fiona (Design), James (Sales). Apologies: none. (1) Welcome and agenda — Ethan confirmed the focus on Q1 delivery and Q2 planning. (2) Product roadmap review — Rachel presented the updated roadmap. Key decisions: (a) Ship AI document generation v2 by 31 March 2026; scope confirmed (templates, streaming preview, PDF export). (b) Defer mobile app to Q3 2026; focus remains web and API. (c) Approve £40K budget for the planned security audit (Q2). (3) Engineering update — Marcus reported API rate limiting design is 80% complete; to be finalised and documented by 12 March. (4) Design — Fiona to deliver updated design system components (buttons, forms, modals) by 14 March for integration. (5) Sales — James noted two enterprise pilots in progress; no change to Q1 revenue forecast. (6) Action items: Marcus — finalise API rate limiting spec and share by 12 March. Rachel — update roadmap in Notion by 7 March and circulate. Fiona — design system components by 14 March. (7) Next meeting: 2 April 2026, 10:00am. Meeting closed at 11:28am. Distribution: attendees and leadership team. Include "Draft" or "Final" as appropriate and space for chair sign-off.
|
||||
|
||||
---
|
||||
|
||||
## nondisclosure_agreement
|
||||
Create a mutual nondisclosure agreement between Apex Renewables Ltd (Company Number 11223344), 30 Green Lane, Bristol BS1 2AB ("Party A"), and Greenfield Energy Partners Ltd (Company Number 55667788), 15 Energy Place, London EC2A 1BB ("Party B"). Purpose: both parties are sharing commercially sensitive information (including technical, financial, and commercial data) to evaluate a potential joint venture for the development and operation of a 50MW solar PV project in Somerset (the "Project"). Definition of Confidential Information: all non-public information disclosed in connection with the Project, whether in writing, orally, or otherwise, and marked or reasonably identifiable as confidential. Exclusions: information that is or becomes publicly available (other than by breach), independently developed, or rightfully received from a third party without restriction. Obligations: each party will hold the other's Confidential Information in confidence, use it only for evaluating and negotiating the Project, and not disclose it except to advisers and personnel on a need-to-know basis under equivalent obligations. Term: 3 years from the date of signing. Return/destruction of materials on request or at end of purpose. Governing law: England and Wales. No licence or obligation to proceed with the Project. Effective date: 1 March 2026. Include signature blocks for both parties.
|
||||
|
||||
---
|
||||
|
||||
## offer_letter
|
||||
Create a job offer letter from Hartwell Digital Agency Ltd, 25 Digital Way, Manchester M1 4BT, to James Okafor, [home address to be inserted]. Position: Lead Software Engineer. Start date: 4 May 2026. Salary: £82,000 per annum, paid monthly in arrears by BACS. Benefits: 25 days annual leave plus bank holidays; private health insurance (after 3 months); pension with 5% employer contribution (employee minimum 3%); annual training and development budget of £2,000. You will report to the Head of Engineering and your principal place of work will be our Manchester office, with hybrid working (3 days in office, 2 days remote) as per our policy. This offer is contingent on: (1) satisfactory references (we will request two, one of which should be from your current or most recent employer); (2) confirmation of your right to work in the UK; (3) no material conflict of interest. Please confirm acceptance in writing by 28 February 2026. If you accept, you will receive a contract of employment and new starter paperwork. We look forward to welcoming you to the team. Sign off from the Head of People or Managing Director. Include space for the candidate's signature and date of acceptance.
|
||||
|
||||
---
|
||||
|
||||
## official_memo
|
||||
Write an official memo from Ethan Scott, CEO, Stirling PDF, to all staff. Date: 1 March 2026. Subject: Company-wide AI usage policy — update effective 1 April 2026. Distribution: all employees and contractors. The memo should state that the company is updating its AI usage policy to protect our clients, our IP, and our compliance posture. Key points: (1) Approved AI tools — only the internal Stirling AI assistant (our own product) and GitHub Copilot (for code) may be used for work. No other external generative AI tools (e.g. public ChatGPT, Claude, etc.) are permitted for company or client work. (2) Customer and confidential data — you must not enter any customer data, PII, or confidential business information into external AI services. Use only approved tools in their approved, compliant configurations. (3) Mandatory training — all staff must complete the new "AI at Stirling" training module in the LMS by 31 March 2026. Incomplete training will be escalated to line managers. (4) AI usage log — for all client-facing work that involves AI-assisted output, you must log the use (tool, date, document/project) in the new AI usage register. This supports our audit and client assurance. Questions to people@stirlingpdf.com. The policy document is available on the intranet. Thank you for your cooperation. Formal memo format; sign off from Ethan Scott, CEO.
|
||||
|
||||
---
|
||||
|
||||
## pay_stub
|
||||
Create a pay stub for Thomas Harrison, Senior Software Engineer, Meridian Digital Ltd, 40 Software Park, Manchester M2 1AB. Employee ID: EMP-00247. Department: Engineering. Pay period: 1 January 2026 to 31 January 2026. Pay date: 31 January 2026. Earnings: basic salary (monthly) £5,200.00; overtime 5 hours at £65.00/hour £325.00; total gross £5,525.00. Deductions: income tax (PAYE) £972.50; National Insurance (employee) £471.25; pension (employee 5%) £276.25; total deductions £1,720.00. Net pay: £3,805.00. Year to date: gross £5,525.00, tax £972.50, NI £471.25, pension £276.25, net £3,805.00. Payment method: BACS to Lloyds Bank, account ending 6742. Tax reference: 123/AB45678. Leave balance: 22 days remaining (as at 31 Jan). This is not a tax certificate; P60/P45 will be issued as applicable. Include company registration number and a note that the pay stub is confidential.
|
||||
|
||||
---
|
||||
|
||||
## performance_review
|
||||
Create a performance review form for Priya Nair, Senior Business Analyst, Cavendish Financial Solutions Ltd. Review period: 1 January 2025 to 31 December 2025. Reviewer/Manager: David Hartley, Director of Consulting. Review date: 15 February 2026. Ratings (1–5 scale: 1 = Below Expectations, 2 = Needs Improvement, 3 = Meets Expectations, 4 = Exceeds Expectations, 5 = Outstanding): Communication — 4; Technical analysis — 5; Stakeholder management — 4; Delivery and execution — 4; Leadership — 3. Overall rating: Exceeds Expectations. Summary of accomplishments: led the delivery of the new client reporting platform, completing 2 weeks ahead of schedule and under budget; reduced standard report turnaround time by 60% through process and tooling improvements; supported 3 major client workshops with strong feedback. Areas for development: leadership and delegation; Priya has taken on more complex work but could develop by line-managing 1–2 junior analysts and leading a small workstream. Goals for 2026: (1) Build leadership skills — take on line management of two junior analysts by Q2; (2) Complete CBAP certification by end of 2026; (3) Lead at least one cross-team initiative. Employee comments (optional): [space for Priya to add]. Sign-off: employee and manager signatures and dates. Confidential — HR to retain.
|
||||
|
||||
---
|
||||
|
||||
## price_sheet
|
||||
Create a price sheet for Vantage Cloud Services Ltd. Document title: Vantage Cloud — Price List. Effective date: 1 March 2026. All prices are in GBP and exclude VAT unless stated. Products and services: Starter Plan (monthly, per seat) — £12; Growth Plan (monthly, per seat) — £29; Enterprise Plan (monthly, per seat) — £59; Professional Services (per day, onsite or remote) — £950; Data Migration (flat fee per migration project) — £2,500; Priority Support SLA (per month, in addition to plan) — £500; Custom Integrations — price on application (POA). Notes: minimum 12-month commitment for Growth and Enterprise; Professional Services sold in half-day blocks; Data Migration includes up to 50GB and one go-live support window. Discounts: annual prepay (10% off plan fees); volume discounts for 50+ seats on request. Contact: sales@vantagecloud.co.uk, 0800 123 4567. Company registration and VAT number at footer. "Prices subject to change; valid for new orders from the effective date."
|
||||
|
||||
---
|
||||
|
||||
## purchase_order
|
||||
Create a purchase order from Crestwood Technologies Ltd, 60 Business Park, Bristol BS2 8CD (Buyer: Procurement, tel. 0117 123 4567), to Apex Office Supplies Ltd, Apex House, Northampton NN3 8AB. PO number: PO-2026-0312. Date: 10 March 2026. Delivery: requested by 20 March 2026 to Crestwood Technologies Ltd, 60 Business Park, Bristol BS2 8CD; contact: Reception. Payment terms: 30 days from date of invoice. Line items: (1) Ergonomic office chairs, model ErgoFlex Pro — quantity 10, unit price £320.00, line total £3,200.00; (2) Height-adjustable standing desks — quantity 4, unit price £540.00, line total £2,160.00; (3) Monitor arms (single, VESA compatible) — quantity 20, unit price £45.00, line total £900.00. Subtotal: £6,260.00. VAT at 20%: £1,252.00. Total: £7,512.00. This PO is subject to our standard terms of purchase (available on request). Please acknowledge receipt and confirm delivery date. Include space for supplier acceptance and reference.
|
||||
|
||||
---
|
||||
|
||||
## quote
|
||||
Create a quote from Blackthorn Digital Ltd, 15 Design Quarter, London SE1 2AB, to Nexus Retail Solutions, 80 Retail Way, Leeds LS1 2AB. Quote number: QT-2026-089. Date: 15 February 2026. Valid for: 30 days (until 17 March 2026). Project: e-commerce redesign — discovery, design, frontend build, QA, and project management. Line items: Discovery and UX research (2 weeks, workshops and user research) — £4,800; UI design (12 key screens, responsive, design system) — £6,000; Frontend development (React, 8 weeks) — £14,400; QA and testing (2 weeks, UAT support) — £2,400; Project management (throughout) — £1,800. Total (excl. VAT): £29,400. VAT at 20%: £5,880. Total (incl. VAT): £35,280. Payment schedule: 30% on kick-off (£8,820 incl. VAT), 40% at mid-project milestone (£14,112 incl. VAT), 30% on delivery and sign-off (£10,584 incl. VAT). Assumptions: content and copy to be provided by client; backend API to be available by week 4; 2 rounds of design revisions included. Next steps: sign the quote and return to proceed; we will then send a contract and project plan. Contact: projects@blackthorndigital.co.uk. Include terms (e.g. scope change process, IP, liability) and signature/acceptance block.
|
||||
|
||||
---
|
||||
|
||||
## receipt
|
||||
Create a receipt from Blackthorn Digital Ltd (VAT GB 123 4567 89), 15 Design Quarter, London SE1 2AB, to Meridian Solutions plc, 100 Commerce House, Manchester M2 5AB. Receipt number: REC-2026-031. Date: 28 February 2026. This receipt confirms payment received from Meridian Solutions plc for the following: Invoice INV-2026-047 — UX design sprint, brand guidelines document, and monthly retainer February 2026. Amount received: £10,440.00 (including VAT). Payment method: BACS transfer. Payment reference: MERI-FEB26. Bank: as per our invoice. Thank you for your business. For any queries contact accounts@blackthorndigital.co.uk. Company registration: 09876543. Include "Paid in full" or similar and optional remittance advice reference.
|
||||
|
||||
---
|
||||
|
||||
## resume
|
||||
Create a resume (CV) for Alexandra Moore, senior product manager, based in London. Contact: alexandra.moore@email.com, 07XXX XXXXXX. Summary: 8 years of experience in B2B SaaS product management; track record of launching and scaling products and leading cross-functional teams. Employment: (1) Head of Product, Stirling PDF, 2023–present — own product strategy and roadmap for the SaaS platform; lead a team of 4 product managers; launched AI document generation feature, contributing to 40% ARR growth in 12 months. (2) Senior Product Manager, Notion, 2020–2023 — owned Notion Templates and integrations roadmap; increased template adoption by 25%. (3) Product Manager, Intercom, 2018–2020 — worked on Messenger and automation products; shipped multiple features used by 10K+ customers. Education: BSc Computer Science, University of Edinburgh, 2015 (First Class Honours). Skills: product strategy, roadmap planning, SQL, Figma, Agile/Scrum, stakeholder management, pricing and packaging. Key achievement: led the launch of Stirling PDF's AI document generation feature from concept to GA; drove adoption and feedback loops that grew ARR by 40% in the first 12 months. Professional memberships or certifications: optional. Format: clear sections, reverse chronological order, one to two pages. Tone: professional and concise.
|
||||
|
||||
---
|
||||
|
||||
## safe_agreement
|
||||
Create a Simple Agreement for Future Equity (SAFE) between Helix Biotech Ltd (Company Number 11223344), 42 Innovation Way, Cambridge CB2 8AB (the "Company"), and Beacon Ventures Fund II LP, 100 Venture Street, London EC2A 1BB (the "Investor"). Investment amount: £500,000 (five hundred thousand pounds). Valuation cap: £8,000,000 (pre-money). Discount rate: 20%. The SAFE will convert into equity on the next qualifying equity financing (equity round with total proceeds of at least £1M), or on a liquidity event or dissolution. MFN (most favoured nation) clause: if the Company issues SAFEs with more favourable terms before conversion, this SAFE will be amended to match. Pro rata rights: the Investor will have the right to participate in the next round on the same terms, up to its pro rata share. Governing law: England and Wales. Effective date: 15 February 2026. Signed for the Company: Dr. Amelia Hargreaves, CEO. Signed for the Investor: Thomas Beacon, Managing Partner, Beacon Ventures Fund II LP. Include definitions (e.g. "Company Capitalisation", "Conversion"), conversion mechanics, and standard boilerplate (no representations, entire agreement, notices).
|
||||
|
||||
---
|
||||
|
||||
## separation_notice
|
||||
Create a separation notice from Aldermoor Logistics plc, 25 Distribution Way, Bristol BS3 2AB, to Marcus Reid, Warehouse Operations Supervisor. Employee ID: EMP-30891. Department: Bristol Distribution Centre. Separation type: redundancy due to site consolidation (closure of Bristol distribution centre; operations moving to Birmingham). Effective date of termination: 28 February 2026. Final pay and entitlements: salary through 28 February 2026; 8 weeks' statutory redundancy pay (as calculated); payment in lieu of 5 days accrued untaken annual leave. Payment will be made on or before 28 February 2026 by BACS to your usual account. Benefits: private health cover will continue through 31 March 2026; you will receive separate communication regarding options thereafter. Equipment and access: you must return by 28 February 2026: company laptop, building access fob, and company vehicle keys (if applicable). Please arrange handover with your line manager Paul Fairweather. Reference: a reference letter will be provided by Paul Fairweather; you may request it from HR after your leaving date. Exit interview: HR will contact you to offer an optional exit interview. We thank you for your service and wish you well. Contact: HR at hr@aldermoorlogistics.co.uk. Include space for employee acknowledgment (optional) and company sign-off. Confidential.
|
||||
|
||||
---
|
||||
|
||||
## service_agreement
|
||||
Create a service agreement between Pinnacle Cloud Services Ltd (Company Number 11223344), 10 Cloud Street, Reading RG1 2AB ("Provider"), and Crestwood Technologies Ltd (Company Number 55667788), 60 Business Park, Bristol BS2 8CD ("Client"). Services: (1) Managed IT support — helpdesk available 9am–6pm Monday–Friday (excluding UK bank holidays); ticket logging, triage, and resolution; (2) Monthly security patching — OS and application patches applied in agreed maintenance windows; (3) Quarterly business reviews — review of service performance, incidents, and roadmap. Scope: up to 50 users and 80 devices as specified in the order form. Monthly fee: £3,200 plus VAT; invoiced in advance; payment within 30 days. Term: 12 months from 1 April 2026; thereafter continuing until terminated. Termination: either party may give 3 months' written notice to take effect at or after the end of the initial term. SLA: P1 (critical) — response within 1 hour, target resolution 4 hours; P2 (high) — response within 4 hours, target resolution 1 business day; P3 (normal) — response within 1 business day. Service credits for repeated SLA failures as per schedule. Data and security: Provider will process Client data in accordance with agreed security standards and UK GDPR. Liability: cap at 12 months of fees. Governing law: England and Wales. Include change control, force majeure, and signature blocks.
|
||||
|
||||
---
|
||||
|
||||
## standard_operating_procedures
|
||||
Create a standard operating procedure (SOP) for incoming raw materials inspection at Meridian Pharma Manufacturing Ltd. SOP number: QA-RM-007. Version: 2.1. Effective date: 1 February 2026. Previous version: 2.0 (dated 1 June 2024). Purpose: to ensure that all incoming raw materials are inspected, sampled, and assessed against approved specifications before release to production, thereby preventing non-conforming materials from entering the manufacturing process. Scope: all raw materials received at the Meridian site (Warehouse Receipt Bay 1 and 2). Responsibilities: Warehouse — receipt and logging; QC — sampling and testing; QA — review and release/reject decision. Procedure: (1) Receipt and logging — on delivery, verify identity and quantity against the purchase order and delivery note; log in the ERP system (transaction GR-RM-001); assign a unique receipt number; place materials in the quarantine area. (2) Visual inspection — check packaging for damage, contamination, or temperature deviation (where applicable); record findings on form QA-RM-007-F1. (3) Sampling — per the approved sampling plan (QA-SP-002), take representative samples; send samples to QC lab with the request form; retain reserve samples. (4) QC testing — QC performs tests per the relevant specifications; results recorded in the LIMS and CoA generated. (5) Review — QA reviews the CoA and packaging inspection against the approved specification; if conforming, approve release in the ERP system (transaction REL-RM-001); if non-conforming, reject and initiate quarantine procedure (QA-RM-008). (6) Quarantine for rejected materials — move to rejected materials area; label clearly; document and notify supplier; disposition per QA-RM-008. References: QA-SP-002 (Sampling Plan), QA-RM-008 (Quarantine and Disposition), ERP user guides. Training: all personnel performing this procedure must be trained and assessed competent; training records in the training matrix. Approved by: Dr. Sarah Okonkwo, QA Manager. Date: 15 January 2026. Next review: 1 February 2027. Include revision history table and document control footer.
|
||||
@@ -1,231 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Separation Notice</title>
|
||||
<style>
|
||||
/* ── Theme variables (replaced at generation time) ── */
|
||||
{{THEME_CSS}}
|
||||
|
||||
/* ── Print / page setup ── */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--theme-font, 'Helvetica Neue', Arial, sans-serif);
|
||||
font-size: 10pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
background: var(--theme-bg, #ffffff);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.document {
|
||||
max-width: 170mm;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
{{HEADER_CSS}}
|
||||
|
||||
.letter-date {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin: 8pt 0 10pt;
|
||||
}
|
||||
|
||||
/* ── Section heading ── */
|
||||
.section-heading {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: var(--theme-primary, #1e3a5f);
|
||||
border-bottom: 1pt solid var(--theme-accent, #2563eb);
|
||||
padding-bottom: 2pt;
|
||||
margin: 10pt 0 6pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Meta grid ── */
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 110pt auto;
|
||||
row-gap: 3pt;
|
||||
margin-bottom: 8pt;
|
||||
border-left: 2pt solid var(--theme-accent, #2563eb);
|
||||
padding-left: 8pt;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 9pt;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
}
|
||||
|
||||
.separation-type-badge {
|
||||
display: inline-block;
|
||||
background: var(--theme-surface, #f8fafc);
|
||||
border: 0.5pt solid var(--theme-border, #e2e8f0);
|
||||
border-left: 3pt solid var(--theme-accent, #2563eb);
|
||||
padding: 2pt 8pt;
|
||||
font-size: 9pt;
|
||||
font-weight: 600;
|
||||
color: var(--theme-heading, #111827);
|
||||
}
|
||||
|
||||
/* ── Text sections ── */
|
||||
.text-section {
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 4pt;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ── Signature block ── */
|
||||
.sig-block {
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.sig-closer {
|
||||
font-size: 9.5pt;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 14pt;
|
||||
}
|
||||
|
||||
.sig-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20pt;
|
||||
margin-top: 8pt;
|
||||
}
|
||||
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
|
||||
.sig-party-label {
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--theme-accent, #2563eb);
|
||||
margin-bottom: 20pt;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-bottom: 0.75pt solid var(--theme-text, #1a1a1a);
|
||||
margin-bottom: 3pt;
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
.sig-field-label {
|
||||
font-size: 7.5pt;
|
||||
color: var(--theme-text-muted, #6b7280);
|
||||
margin-bottom: 10pt;
|
||||
}
|
||||
|
||||
/* ── Page-break controls ── */
|
||||
thead { display: table-header-group; }
|
||||
tr { break-inside: avoid; page-break-inside: avoid; }
|
||||
.section-heading { break-after: avoid; page-break-after: avoid; }
|
||||
.sig-block { break-inside: avoid; page-break-inside: avoid; }
|
||||
|
||||
/* ── Print overrides ── */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.document { max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
<div class="top-bar"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="doc-header">
|
||||
{{LOGO_BLOCK}}
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Separation Notice</div>
|
||||
<div class="doc-subtitle">{{COMPANY_NAME}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Company Address:</span> <span class="meta-value">{{COMPANY_ADDRESS}}</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">{{LETTER_DATE}}</span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Employee Information -->
|
||||
<div class="section-heading">Employee Information</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Employee Name:</span> <span class="meta-value">{{EMPLOYEE_NAME}}</span>
|
||||
<span class="meta-label">Employee ID:</span> <span class="meta-value">{{EMPLOYEE_ID}}</span>
|
||||
<span class="meta-label">Position:</span> <span class="meta-value">{{POSITION}}</span>
|
||||
<span class="meta-label">Department:</span> <span class="meta-value">{{DEPARTMENT}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Separation Details -->
|
||||
<div class="section-heading">Separation Details</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Separation Type:</span> <span class="meta-value"><span class="separation-type-badge">{{SEPARATION_TYPE}}</span></span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">{{EFFECTIVE_DATE}}</span>
|
||||
</div>
|
||||
<div class="text-section">{{SEPARATION_REASON_TEXT}}</div>
|
||||
|
||||
<!-- Final Pay -->
|
||||
<div class="section-heading">Final Pay</div>
|
||||
<div class="text-section">{{FINAL_PAY_TEXT}}</div>
|
||||
|
||||
<!-- Benefits -->
|
||||
<div class="section-heading">Benefits</div>
|
||||
<div class="text-section">{{BENEFITS_TEXT}}</div>
|
||||
|
||||
<!-- Equipment Return -->
|
||||
<div class="section-heading">Return of Company Property</div>
|
||||
<div class="text-section">{{EQUIPMENT_TEXT}}</div>
|
||||
|
||||
<!-- References -->
|
||||
<div class="section-heading">References Policy</div>
|
||||
<div class="text-section">{{REFERENCES_TEXT}}</div>
|
||||
|
||||
<!-- Closing -->
|
||||
<div class="section-heading">Closing Statement</div>
|
||||
<div class="sig-block">
|
||||
<div class="sig-closer">{{CLOSING_TEXT}}</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">HR Representative</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{HR_SIGNER_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: {{HR_SIGNER_TITLE}}</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee Acknowledgement</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: {{EMPLOYEE_NAME}}</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: {{EMPLOYEE_ACK_DATE}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,115 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Separation Notice</title>
|
||||
<style>
|
||||
:root {
|
||||
--theme-primary: #1e3a5f;
|
||||
--theme-accent: #2563eb;
|
||||
--theme-secondary: #475569;
|
||||
--theme-bg: #ffffff;
|
||||
--theme-surface: #f8fafc;
|
||||
--theme-border: #e2e8f0;
|
||||
--theme-text: #1a1a1a;
|
||||
--theme-text-muted: #6b7280;
|
||||
--theme-heading: #111827;
|
||||
--theme-font: 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
@page { size: A4; margin: 20mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: var(--theme-font); font-size: 10pt; color: var(--theme-text); background: var(--theme-bg); line-height: 1.4; }
|
||||
.document { max-width: 170mm; margin: 0 auto; }
|
||||
.top-bar { height: 3pt; background: var(--theme-accent); margin-bottom: 10pt; }
|
||||
.doc-header { display: flex; align-items: center; gap: 12pt; margin-bottom: 8pt; }
|
||||
.doc-header-text { flex: 1; text-align: center; }
|
||||
.doc-title { font-size: 16pt; font-weight: 700; color: var(--theme-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3pt; }
|
||||
.doc-subtitle { font-size: 9pt; color: var(--theme-text-muted); }
|
||||
.section-heading { font-size: 10pt; font-weight: 700; color: var(--theme-primary); border-bottom: 1pt solid var(--theme-accent); padding-bottom: 2pt; margin: 10pt 0 6pt; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.meta-grid { display: grid; grid-template-columns: 110pt auto; row-gap: 3pt; margin-bottom: 8pt; border-left: 2pt solid var(--theme-accent); padding-left: 8pt; }
|
||||
.meta-label { font-weight: 600; color: var(--theme-text-muted); font-size: 9pt; }
|
||||
.meta-value { font-size: 9pt; color: var(--theme-text); }
|
||||
.separation-type-badge { display: inline-block; background: var(--theme-surface); border: 0.5pt solid var(--theme-border); border-left: 3pt solid var(--theme-accent); padding: 2pt 8pt; font-size: 9pt; font-weight: 600; color: var(--theme-heading); }
|
||||
.text-section { font-size: 9.5pt; line-height: 1.6; color: var(--theme-text); margin-bottom: 4pt; white-space: pre-line; }
|
||||
.sig-block { margin-top: 14pt; }
|
||||
.sig-closer { font-size: 9.5pt; line-height: 1.6; margin-bottom: 14pt; }
|
||||
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20pt; margin-top: 8pt; }
|
||||
.sig-col { font-size: 8.5pt; }
|
||||
.sig-party-label { font-size: 8pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--theme-accent); margin-bottom: 20pt; }
|
||||
.sig-line { border-bottom: 0.75pt solid var(--theme-text); margin-bottom: 3pt; height: 14pt; }
|
||||
.sig-field-label { font-size: 7.5pt; color: var(--theme-text-muted); margin-bottom: 10pt; }
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="top-bar"></div>
|
||||
<div class="doc-header">
|
||||
<div class="doc-header-text">
|
||||
<div class="doc-title">Separation Notice</div>
|
||||
<div class="doc-subtitle">Aldermoor Logistics plc</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Company Address:</span> <span class="meta-value">12 Wharf Street, Bristol, BS1 4RN</span>
|
||||
<span class="meta-label">Date:</span> <span class="meta-value">3 February 2026</span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="section-heading">Employee Information</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Employee Name:</span> <span class="meta-value">Marcus Reid</span>
|
||||
<span class="meta-label">Employee ID:</span> <span class="meta-value">EMP-1134</span>
|
||||
<span class="meta-label">Position:</span> <span class="meta-value">Warehouse Operations Supervisor</span>
|
||||
<span class="meta-label">Department:</span> <span class="meta-value">Distribution & Fulfilment</span>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Separation Details</div>
|
||||
<div class="meta-grid">
|
||||
<span class="meta-label">Separation Type:</span> <span class="meta-value"><span class="separation-type-badge">Redundancy</span></span>
|
||||
<span class="meta-label">Effective Date:</span> <span class="meta-value">28 February 2026</span>
|
||||
</div>
|
||||
<div class="text-section">Following the completion of the company's operational restructuring review, the role of Warehouse Operations Supervisor at the Bristol Wharf site has been identified as redundant. This decision follows a full and fair consultation process conducted between 5 January and 28 January 2026. All reasonable steps to identify suitable alternative employment within the business were taken; regrettably, no suitable alternative position was available. This notice is therefore issued in accordance with your statutory and contractual rights.</div>
|
||||
|
||||
<div class="section-heading">Final Pay</div>
|
||||
<div class="text-section">Your final salary payment, including all accrued but untaken annual leave (7 days, equivalent to £1,346.15), will be processed in the February 2026 payroll run and credited to your bank account on 28 February 2026. You are entitled to a statutory redundancy payment of £4,800, calculated on the basis of your continuous service of 6 years. A written breakdown of all final payments will be provided with your payslip.</div>
|
||||
|
||||
<div class="section-heading">Benefits</div>
|
||||
<div class="text-section">Your private medical insurance cover through AXA Health will remain active until 28 February 2026. Pension contributions will cease on your effective leaving date; your pension provider, Nest, will write to you separately regarding the options available for your accumulated pension pot. Access to the employee assistance programme helpline will continue for 30 days following your leaving date.</div>
|
||||
|
||||
<div class="section-heading">Return of Company Property</div>
|
||||
<div class="text-section">You are required to return all company property in your possession before or on 28 February 2026. This includes, but is not limited to: your company-issued access fob and car park pass, any keys to company premises or vehicles, your company mobile telephone and any associated accessories, and any confidential documents or records (whether in physical or digital form). Please arrange the return of these items with your line manager, Sandra Hughes, no later than 26 February 2026.</div>
|
||||
|
||||
<div class="section-heading">References Policy</div>
|
||||
<div class="text-section">The company's policy is to provide a factual reference confirming dates of employment and job title only. References should be requested through HR at hr@aldermoorlogistics.co.uk. The company will not provide references that include subjective assessments of performance.</div>
|
||||
|
||||
<div class="section-heading">Closing Statement</div>
|
||||
<div class="sig-block">
|
||||
<div class="sig-closer">We wish to thank Marcus for his contribution to Aldermoor Logistics over the past six years and wish him well in his future career. Should you have any questions regarding this notice or your entitlements, please contact the HR team at hr@aldermoorlogistics.co.uk or on 0117 900 4500.</div>
|
||||
<div class="sig-grid">
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">HR Representative</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Rachel Thornton</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Title: HR Business Partner</div>
|
||||
</div>
|
||||
<div class="sig-col">
|
||||
<div class="sig-party-label">Employee Acknowledgement</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Signature</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Name: Marcus Reid</div>
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-field-label">Date: 3 February 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user