ai-bw template refinements: remove page-2 header, add project spacing, add levit profile
Some checks failed
CI / core (push) Failing after 10m33s
Some checks failed
CI / core (push) Failing after 10m33s
- Remove page-2 running header (was overlapping EARLIER CAREER heading) - Add inter-entry spacing to Projects section to match Earlier Career rhythm - Add levit.yml resume profile - Add export scripts for levit in package.json - Add work arrangement legend text-icon support in options.js Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
234a8866b1
commit
9a530f941c
@ -15,6 +15,7 @@
|
||||
"start": "node --openssl-legacy-provider build/dev-server.js",
|
||||
"dev:dobkin": "RESUME_NAME=dobkin node --openssl-legacy-provider build/dev-server.js",
|
||||
"dev:cherepaha": "RESUME_NAME=cherepaha node --openssl-legacy-provider build/dev-server.js",
|
||||
"dev:levit": "RESUME_NAME=levit node --openssl-legacy-provider build/dev-server.js",
|
||||
"pdf": "node scripts/export.js",
|
||||
"pdf:green": "node scripts/export.js green",
|
||||
"pdf:ai-bw": "node scripts/export.js ai-bw",
|
||||
@ -33,6 +34,8 @@
|
||||
"export:cherepaha:ai-bw": "RESUME_NAME=cherepaha concurrently \"npm run dev\" \"node scripts/export.js ai-bw\" --success first --kill-others --raw",
|
||||
"export:bw": "node scripts/export-cli.js",
|
||||
"export:cherepaha": "RESUME_NAME=cherepaha concurrently \"npm run dev\" \"npm run pdf\" --success first --kill-others --raw",
|
||||
"export:levit": "RESUME_NAME=levit concurrently \"npm run dev\" \"npm run pdf\" --success first --kill-others --raw",
|
||||
"export:levit:ai-bw": "RESUME_NAME=levit concurrently \"npm run dev\" \"node scripts/export.js ai-bw\" --success first --kill-others --raw",
|
||||
"lint": "eslint --ext .js,.vue src scripts",
|
||||
"lint:fix": "eslint --ext .js,.vue src scripts --fix"
|
||||
},
|
||||
|
||||
@ -2,165 +2,169 @@
|
||||
# Experience: timeperiod = dates only; location = separate line. description: | = one bullet per line.
|
||||
# Optional: remote: true | false | hybrid; employment: contract | full-time | co-op (icons + legend on green/purple).
|
||||
# Optional contact: github_profile = public GitHub.com username (add if you use both Forge + GitHub).
|
||||
#
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STACK TOGGLE
|
||||
# Flip show_stack below to hide/show ALL per-role "Stack:" lines at once.
|
||||
# Set to false to hide all Stack: lines across the resume. Default: true (show).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
show_stack: true
|
||||
|
||||
name:
|
||||
first: ILIA
|
||||
middle:
|
||||
last: DOBKIN
|
||||
about: "Senior Software Development Engineer in Test with 20+ years across audit/financial platforms and modern regulated web, including real-money iGaming. Experienced owning Playwright/Cypress/Selenium automation across UI, API, and performance, plus CI/CD pipelines and DevOps tooling. Known for test-pyramid and shift-left practices, stabilizing flaky suites, and improving pipeline reliability in high-availability environments. Proactively eliminates repetitive work across teams through scripting, scheduled jobs, and workflow automation—freeing colleagues to focus on higher-value tasks. Improved end-to-end regression cycle time by ~40% by expanding automated coverage and parallelizing suites in CI pipelines."
|
||||
core_strengths: ""
|
||||
about: "Senior SDET with 20+ years in audit/financial software and regulated web, including real-money iGaming. Deep across Playwright, Swagger/OpenAPI contract testing, and performance baselines integrated into CI/CD. I treat automation as a personal discipline as much as a job: scripts, shortcuts, and agents that streamline my day so engineering effort goes where it matters. Strong instinct for stabilizing flaky suites, tightening quality gates, and removing manual regression effort wherever it lives."
|
||||
position: Senior Software Development Engineer in Test (SDET)
|
||||
|
||||
# Shown next to the map pin in the header (green / ai-bw). Other themes use it as a location line; leave year empty to show "Based in …" instead of "Born … in …".
|
||||
work_auth: "Canadian citizen"
|
||||
|
||||
birth:
|
||||
year:
|
||||
location: Toronto, Ontario, Canada
|
||||
location: "Remote (ET)"
|
||||
|
||||
experience:
|
||||
- company: Niyasoft Canada Inc.
|
||||
- company: Niyasoft
|
||||
position: Senior Quality Assurance Automation Engineer
|
||||
timeperiod: August 2023 - April 2026
|
||||
timeperiod: August 2023 – April 2026
|
||||
remote: true
|
||||
employment: full-time
|
||||
location: Vaughan, Ontario, Canada
|
||||
stack: "Playwright, TypeScript, GitHub Actions, PostgreSQL, GCP"
|
||||
description: |
|
||||
• Built and maintained 300+ Playwright E2E tests and 250+ API/integration tests (Swagger/OpenAPI) plus Artillery performance suites for a regulated online-casino platform; coverage spanned happy-path, negative, workflow, page-navigation, and network request/response checks across payments, wallet/cashier, game/lobby, and back-office flows—cutting manual regression effort by ~50% and catching regressions earlier in the pipeline.
|
||||
• Stabilized the Playwright suite by replacing brittle waits with deterministic patterns and improving environment readiness, cutting flaky-test noise and keeping daily pass rates consistently above ~95% across parallel CI stages.
|
||||
• Validated responsible gaming and player protection end-to-end—deposit/loss/session limits, self-exclusion, cooling-off, reality checks—supporting successful compliance posture across licensed wagering markets.
|
||||
• Ran compliance-sensitive scenarios for geo-eligibility, age-gating, and restricted jurisdictions with audit-friendly logging; traceability artifacts available for licensing reviews on demand.
|
||||
• Optimized GitHub Actions pipelines (regression, functional, component, smoke) with parallelized stages and daily PR/review cadence, keeping feedback time short on a high-availability real-money stack.
|
||||
• Monitored API reliability via GCP, Prometheus, and alerting; validated PostgreSQL-backed data integrity and prevented sev-1 incidents by catching performance regressions with Artillery baselines before release.
|
||||
• Built and maintained **300+ Playwright E2E tests** and 250+ API/integration tests plus performance suites for a regulated online-casino platform; coverage spanned happy-path, negative, workflow, page-navigation, and network request/response checks across payments, wallet/cashier, game/lobby, and back-office flows—cutting manual regression effort by 50% and catching regressions earlier in the pipeline.
|
||||
• Stabilized the **Playwright suite** by replacing brittle waits with deterministic patterns and improving environment readiness; reduced flaky-test noise and maintained daily pass rates above 90% across parallel CI stages.
|
||||
• Validated **responsible gaming and player protection** end-to-end—deposit/loss/session limits, self-exclusion, cooling-off, reality checks—supporting successful compliance posture across licensed wagering markets.
|
||||
• Ran compliance-sensitive scenarios for **geo-eligibility** with audit-friendly logging; traceability artifacts available for licensing reviews on demand.
|
||||
• Optimized **GitHub Actions pipelines** (regression, functional, component, smoke) with parallelized stages and daily PR/review cadence, keeping feedback time short on a high-availability real-money stack.
|
||||
• Monitored GCP metrics and alerts for API reliability; validated **PostgreSQL-backed data integrity** and prevented sev-1 incidents by catching performance regressions before release.
|
||||
• Authored and enforced **Swagger/OpenAPI contract tests** against backend microservices, catching breaking schema changes before they reached downstream consumers.
|
||||
|
||||
- company: RIOS Canada
|
||||
position: Software Development Engineer in Test (SDET)
|
||||
timeperiod: June 2022 - July 2023
|
||||
timeperiod: June 2022 – July 2023
|
||||
remote: true
|
||||
employment: contract
|
||||
location: Toronto, Ontario, Canada
|
||||
stack: "Cypress, JavaScript, Bitbucket CI, Swagger/OpenAPI, Ansible"
|
||||
description: |
|
||||
• Built Cypress E2E and API suites (Swagger/OpenAPI) from scratch across core product flows; owned patterns, fixtures, and stability, enabling every-commit checks and cutting manual regression time by ~40% per release.
|
||||
• Introduced AODA/WCAG accessibility checks (alt text, keyboard nav, contrast) into Bitbucket CI gates, preventing accessibility regressions across web and mobile releases.
|
||||
• Automated test-environment provisioning with Ansible, producing disposable, repeatable setups that shortened spin-up time and eliminated pre-regression drift.
|
||||
• Partnered with engineering and product on defect triage, risk-based prioritization, and pragmatic quality gates without blocking incremental delivery.
|
||||
• Built **Cypress E2E and API suites** (Swagger/OpenAPI) from scratch across core product flows, including shared test data builders and fixture libraries—enabling every-commit CI checks and cutting manual regression time by 40% per release.
|
||||
• Introduced **AODA/WCAG accessibility checks** (alt text, keyboard nav, contrast) into Bitbucket CI gates, preventing accessibility regressions across web and mobile releases.
|
||||
• Automated test-environment provisioning with **Ansible**, producing disposable, repeatable setups that shortened spin-up time and eliminated pre-regression drift.
|
||||
• Partnered with engineering and product on **defect triage**, risk-based prioritization, and pragmatic quality gates without blocking incremental delivery.
|
||||
|
||||
- company: Attabotics
|
||||
position: QA Automation Developer
|
||||
timeperiod: September 2021 - May 2022
|
||||
timeperiod: September 2021 – May 2022
|
||||
remote: true
|
||||
employment: contract
|
||||
location: Calgary, Alberta, Canada
|
||||
stack: "SpecFlow, Gherkin, C#, .NET, Azure, Docker, SQL Server"
|
||||
description: |
|
||||
• Maintained 3,500+ SpecFlow/Gherkin scenarios with C# in .NET/Azure, owning flaky-test triage and keeping daily build stability above ~90% across the suite.
|
||||
• Practiced left-shift QA in a large Agile team: co-authored scenarios with developers early in sprint, tightened Given/When/Then clarity, and shortened feedback from story to green build.
|
||||
• Stood up Docker-based local and CI-aligned test environments; used SQL Server for data setup, assertions, and traceability across integrated warehouse-automation workflows.
|
||||
• **Mentored developers** on **testable design** and BDD best practices, improving scenario quality and reducing review churn on test PRs.
|
||||
• Maintained **3,500+ SpecFlow/Gherkin scenarios** with C# in .NET/Azure, owning flaky-test triage and keeping daily build stability above 90% across the suite.
|
||||
• Caught defects earlier and tightened **Given/When/Then** clarity by co-authoring scenarios with developers early in the sprint, shortening feedback from story to green build in a large Agile team (**left-shift QA**).
|
||||
• Stood up **Docker-based local and CI-aligned test environments**; used SQL Server for data setup, assertions, and traceability across integrated warehouse-automation workflows.
|
||||
|
||||
- company: Levkin Inc.
|
||||
- company: Levkin
|
||||
position: Senior Software Developer
|
||||
timeperiod: October 2020 - August 2021
|
||||
timeperiod: October 2020 – August 2021
|
||||
remote: true
|
||||
employment: contract
|
||||
location: Vaughan, Ontario, Canada
|
||||
stack: "Playwright, GitLab CI/CD, Terraform, AWS S3, Grafana"
|
||||
description: |
|
||||
• Built reusable Playwright building blocks with deterministic patterns, eliminating arbitrary waits and measurably reducing suite flakiness.
|
||||
• Built reusable **Playwright** patterns that replaced arbitrary waits, measurably reducing suite flakiness and stabilizing CI runs.
|
||||
• Audited and refactored legacy test and UI code; documented testing strategy and shared patterns across the team.
|
||||
• Optimized GitLab CI/CD pipelines for speed and reliability; piped test and pipeline metrics into Grafana dashboards for release visibility.
|
||||
• Provisioned AWS (S3) environments with Terraform, validated end-to-end, and promoted to dev via the team's release procedure.
|
||||
• Ongoing: self-hosted infrastructure lab and local-GPU AI assistant under the Levkin brand—see Projects section.
|
||||
• Optimized **GitLab CI/CD** pipelines for speed and reliability; piped test and pipeline metrics into Grafana dashboards for release visibility.
|
||||
• Provisioned AWS (S3) environments with **Terraform**, validated end-to-end, and promoted to dev via the team's release procedure.
|
||||
• Introduced **page object patterns and shared utility layer** that cut new-test authoring time and improved cross-team consistency.
|
||||
• Ongoing: self-hosted infrastructure lab and local-GPU AI projects—see Projects section.
|
||||
|
||||
- company: Accountants Templates Inc.
|
||||
position: Senior Software Developer
|
||||
timeperiod: August 2019 - August 2020
|
||||
remote: true
|
||||
employment: contract
|
||||
location: Calgary, Alberta, Canada
|
||||
description: |
|
||||
• Owned CaseWare/CaseView template delivery including compliance updates, standards-driven releases, and documentation for internal and client use.
|
||||
• Reviewed software for improvements and implemented recommendations and collaborated with support on reported issues.
|
||||
• Automated build and packaging workflows with scripting, compressing roughly eight hours of manual release effort down to under two minutes per cycle.
|
||||
- company: EARLIER CAREER
|
||||
timeperiod: 2009 – 2020
|
||||
|
||||
- company: MNP LLP
|
||||
position: Senior Application Developer 2
|
||||
timeperiod: August 2017 - June 2019
|
||||
remote: true
|
||||
employment: full-time
|
||||
location: Toronto, Ontario, Canada
|
||||
description: |
|
||||
• Developed and maintained C# / .NET / .NET Core applications integrating with CaseWare/CaseView; extended with JavaScript where specs required.
|
||||
• Contributed automation strategy and hands-on Selenium/Cucumber work; managed Jenkins triggers, Cucumber reporting, and Azure DevOps pipelines.
|
||||
|
||||
- company: CaseWare International Inc.
|
||||
position: Software Developer
|
||||
timeperiod: August 2006 - June 2017
|
||||
- company: CaseWare International
|
||||
remote: hybrid
|
||||
employment: full-time
|
||||
location: Toronto, Ontario, Canada
|
||||
stack: "C#, .NET, SQL Server, SilkTest, Agile/Scrum"
|
||||
description: |
|
||||
• Delivered features, defect fixes, and client templates (JavaScript, HTML, YUI, jQuery, CSS) for global financial/audit systems; automated validation with SilkTest.
|
||||
• Mentored junior developers on conventions, debugging, and code review; built reusable JS libraries; Agile Scrum, Jira, Git.
|
||||
11 years of feature development, client templates, and SilkTest automation for global audit/financial systems; mentored juniors.
|
||||
|
||||
- company: MNP
|
||||
remote: true
|
||||
employment: full-time
|
||||
stack: "C#, .NET Core, Selenium, Cucumber, Jenkins, Azure DevOps"
|
||||
description: |
|
||||
.NET development on CaseWare/CaseView with Selenium/Cucumber automation across Jenkins and Azure DevOps pipelines.
|
||||
|
||||
- company: Accountants Templates
|
||||
remote: true
|
||||
employment: contract
|
||||
stack: "CaseWare/CaseView, build automation, scripting"
|
||||
description: |
|
||||
CaseWare/CaseView template delivery with build/packaging scripts that cut release effort from ~8 hours to under 2 minutes.
|
||||
|
||||
- company: ROLI Consulting
|
||||
position: Web/Application Developer
|
||||
timeperiod: January 2001 - July 2012
|
||||
remote: true
|
||||
employment: part-time
|
||||
location: Vaughan, Ontario, Canada
|
||||
employment: contract
|
||||
stack: "Python, Twilio API, multi-stack web"
|
||||
description: |
|
||||
• Voice broadcasting and SMS service (Python, Twilio API); websites across multiple stacks; technical consulting for nonprofits and SMBs.
|
||||
|
||||
- company: Earlier Career
|
||||
timeperiod: May 2005 - August 2006
|
||||
sub_companies:
|
||||
- name: Kaboose Inc.
|
||||
remote: false
|
||||
employment: contract
|
||||
- name: Coutts Information Services
|
||||
remote: false
|
||||
employment: full-time
|
||||
- name: EDS / Scotiabank
|
||||
remote: false
|
||||
employment: co-op
|
||||
description: |
|
||||
• QA automation with QTP/Quality Center, cross-browser and end-to-end testing, and UAT support (Kaboose); Java/J2EE development (Coutts); Informatica ETL co-op on AIX/DB2 (EDS/Scotiabank).
|
||||
Voice/SMS broadcasting service plus multi-stack web and technical consulting for nonprofits and SMBs.
|
||||
|
||||
education: []
|
||||
|
||||
# Skills: grouped, deduped, ATS-friendly. Two-column grid in green template.
|
||||
skills:
|
||||
- name: "Test automation: Playwright, Cypress, Selenium, SilkTest; UI, API, mobile, cross-browser; page object model, BDD"
|
||||
- name: "**Test automation**: Playwright, Cypress, Selenium, SilkTest; UI, API, mobile, cross-browser; page object model, BDD"
|
||||
level: 96
|
||||
- name: "Languages & frameworks: TypeScript, JavaScript, C#, .NET, Python, Java, Bash/Shell, Node.js, ASP.NET, Spring Boot, HTML/CSS"
|
||||
- name: "**Domains**: regulated iGaming (real-money), audit & financial software, warehouse automation, accessibility-compliant web (AODA/WCAG)"
|
||||
level: 88
|
||||
- name: "**Languages & frameworks**: TypeScript, JavaScript, C#, .NET, Python, Java, Bash/Shell, Node.js, ASP.NET, Spring Boot, HTML/CSS"
|
||||
level: 92
|
||||
- name: "CI/CD & DevOps: GitHub Actions, GitLab, Bitbucket, Jenkins, Azure DevOps; Git, Terraform, Ansible, Docker, SonarQube, self-hosted runners"
|
||||
- name: "**CI/CD & DevOps**: GitHub Actions, GitLab, Bitbucket, Jenkins, Azure DevOps; Git, Terraform, Ansible, Docker, SonarQube, self-hosted runners"
|
||||
level: 92
|
||||
- name: "Cloud & infra: AWS (Lambda, S3), Azure, GCP; Linux administration, Proxmox, Caddy, TrueNAS, Vaultwarden"
|
||||
- name: "**Cloud & infra**: AWS (Lambda, S3), Azure, GCP; Linux administration, Proxmox, Caddy, TrueNAS, Vaultwarden"
|
||||
level: 84
|
||||
- name: "Observability & performance: Grafana, Prometheus, Sentry, DataDog, Artillery, k6, JMeter, metrics & logging"
|
||||
- name: "**Observability & performance**: Grafana, Prometheus, Sentry, DataDog, Artillery, k6, JMeter, metrics & logging"
|
||||
level: 86
|
||||
- name: "Data & domain: PostgreSQL, SQL Server, MySQL, DB2, Informatica/ETL; CaseWare/CaseView, audit & financial software"
|
||||
- name: "**Data & domain**: PostgreSQL, SQL Server, MySQL, DB2, Informatica/ETL"
|
||||
level: 78
|
||||
- name: "QA practices: BDD (SpecFlow, Cucumber, Gherkin), API testing (Postman, Swagger/OpenAPI), accessibility (AODA/WCAG), risk-based testing and prioritization, defect triage, quality gates, flaky-suite stabilization, test data and fixtures, Agile/Scrum, Jira, shift-left QA"
|
||||
- name: "**QA testing types**: unit, integration, regression, smoke, exploratory, load, stress, end-to-end; API testing (Postman); accessibility (AODA/WCAG)"
|
||||
level: 90
|
||||
- name: "Leadership & collaboration: mentoring developers, test strategy and documentation, partnering with product and engineering on quality planning, release risk, and pragmatic gates without blocking delivery"
|
||||
level: 84
|
||||
- name: "AI & LLM tooling: AI-assisted engineering with Cursor, GitHub Copilot, Perplexity, Claude Code, ChatGPT, Windsurf, and Amazon Q Developer; GenAI-assisted test design and refactors; prompt and workflow validation; privacy-first local LLM usage; MCP servers and agent-based automation"
|
||||
- name: "**QA process**: BDD, risk-based prioritization, defect triage, quality gates, flaky-suite stabilization, shift-left QA, Agile/Scrum, Jira"
|
||||
level: 88
|
||||
- name: "**AI & LLM tooling**: AI-assisted engineering with Cursor and Claude Code; privacy-first local LLM usage; MCP servers and agent-based automation; GenAI-assisted test design and refactors."
|
||||
level: 82
|
||||
|
||||
knowledge:
|
||||
|
||||
projects:
|
||||
- name: Levkin — Self-Hosted Infrastructure Lab
|
||||
- name: "Self-Hosted Infrastructure Lab"
|
||||
stack: "Proxmox, Ansible, Caddy, TrueNAS, Gitea, SonarQube"
|
||||
description: |
|
||||
• Proxmox-based homelab (VMs/LXC): Gitea + CI runners, Vaultwarden, Vikunja, Uptime Kuma, Mailcow, Listmonk, n8n, SonarQube—provisioned via Ansible, Caddy edge TLS, TrueNAS backups.
|
||||
• Full Linux admin: multi-domain DNS, firewall hardening, monitoring, and repeatable deploys; patterns from this lab directly informed production DevOps decisions in later roles.
|
||||
• Proxmox homelab (VMs/LXC) with Gitea, CI runners, Vaultwarden, Uptime Kuma, Mailcow, SonarQube—**Ansible**-provisioned, Caddy TLS, TrueNAS backups; patterns directly informed production DevOps decisions.
|
||||
|
||||
- name: Levkin — Privacy-First Local AI Assistant
|
||||
- name: "sdetProfile — Portfolio as Playwright Report"
|
||||
stack: "HTML, CSS, vanilla JS, Playwright (tests), ESLint, Stylelint"
|
||||
description: |
|
||||
• Tool-using assistant wired into mail and calendars (triage, drafts, scheduling) on local GPU inference—prompts and context stay on-LAN, no SaaS LLMs.
|
||||
• Composable with homelab identity, TLS, secrets, and automation so event-driven workflows hand off without third-party model APIs; used daily for personal productivity.
|
||||
• Zero-framework personal portfolio styled as a **Playwright** test runner—sidebar explorer, editor tabs, trace/network/source panels, tag filtering, keyboard shortcuts, and theme cycling (dark/light/WCAG AAA); 37 real Playwright specs verify the live site.
|
||||
|
||||
- name: Levkin — Playwright MCP server
|
||||
- name: "Atlas — Local Voice Agent"
|
||||
stack: "Python, MCP, ASR/TTS, local LLM (RTX), Playwright"
|
||||
description: |
|
||||
• Built an MCP server for developers to use from Cursor and other MCP-capable assistants while writing Playwright tests—surfacing selectors, fixtures, and in-repo conventions so generated specs stay aligned with team patterns.
|
||||
• Privacy-focused home voice assistant with on-device AI transcription and tool use—**Python**, local GPU inference, no third-party model APIs; wired into calendar and home automation.
|
||||
|
||||
- name: "AtAnyRate — Event-Driven Pricing"
|
||||
stack: "Python, Playwright, Telegram Bot API, Ticketmaster/SeatGeek APIs, Docker"
|
||||
description: |
|
||||
• **Python** app that identifies Toronto events likely to spike Airbnb demand, sends Telegram alerts, and optionally adjusts nightly prices via **Playwright** browser automation.
|
||||
|
||||
- name: "LLM Council — Multi-Model Chat UI"
|
||||
stack: "Python, FastAPI, httpx, React, Vite, Ollama/vLLM"
|
||||
description: |
|
||||
• Local web UI that fans each prompt to multiple LLMs and presents side-by-side responses—**Python** backend, diverse model voting for higher-confidence answers.
|
||||
|
||||
hobbies: []
|
||||
|
||||
@ -168,9 +172,9 @@ contributions: []
|
||||
|
||||
contact:
|
||||
email: idobkin@gmail.com
|
||||
phone: +1 (647) 987-2792
|
||||
phone:
|
||||
street:
|
||||
city: Toronto, Ontario, Canada
|
||||
city:
|
||||
website: https://www.linkedin.com/in/idobkin/
|
||||
website_label: LinkedIn
|
||||
github: https://git.levkin.ca
|
||||
|
||||
144
resume/levit.yml
Normal file
144
resume/levit.yml
Normal file
@ -0,0 +1,144 @@
|
||||
/* #*/ export const PERSON = `
|
||||
# experience: timeperiod = dates; location = separate line; description: | = one bullet per line.
|
||||
# Optional: remote: true | false | hybrid; employment: contract | full-time | co-op
|
||||
# Build: RESUME_NAME=levit npm run dev | PDF: RESUME_NAME=levit npm run export
|
||||
|
||||
name:
|
||||
first: BORIS
|
||||
middle:
|
||||
last: LEVIT
|
||||
|
||||
position: Enterprise / Security / Data / AI Architect (CISO Track)
|
||||
|
||||
about: "CISSP-certified Enterprise Security Architect and DevSecOps/MLSecOps leader with 20+ years delivering security modernization for public sector and regulated enterprises. Designs and operationalizes zero-trust and cloud-native security platforms across AWS/Azure/GCP, integrating SIEM/SOAR, EDR/XDR, identity governance/PAM, and data/AI security. Strong track record producing actionable TRAs/PIAs, executive-ready roadmaps, and operating models that connect NIST/ISO frameworks to measurable controls, telemetry, and incident readiness."
|
||||
|
||||
core_strengths: "Zero Trust & SSE/SASE; cloud security architecture; SIEM/SOAR modernization; DevSecOps & secure CI/CD; IAM/PAM & IGA; AI/LLM security & governance; threat modeling & risk assessments; public-sector security standards."
|
||||
|
||||
birth:
|
||||
year:
|
||||
location: Toronto, Ontario, Canada
|
||||
|
||||
experience:
|
||||
- company: InTunnel Monitor
|
||||
position: Enterprise / Security / Data / AI Architect, DevSecOps Lead
|
||||
timeperiod: September 2017 - Current
|
||||
remote: hybrid
|
||||
employment: contract
|
||||
location: Toronto, ON, Canada
|
||||
description: |
|
||||
• Led architecture and delivery for cybersecurity modernization engagements across federal/provincial clients and regulated enterprises (including OSFI, GC DND, and Ontario Securities Commission).
|
||||
• Designed and operationalized zero-trust and SSE/SASE solutions across hybrid and multi-cloud (AWS/Azure/GCP), including ZTNA, CASB, SWG, FWaaS, and SaaS posture management patterns.
|
||||
• Modernized SecOps by integrating SIEM + SOAR with EDR/XDR/DLP and threat intelligence (ArcSight/Splunk/Sentinel + SOAR patterns), enabling automated playbooks for phishing, identity events, and vulnerability response.
|
||||
• Delivered TRAs/PIAs, security roadmaps, and capability assessments aligned to NIST CSF v2, CIS Controls v8, ISO 27001/27701, ITSG-33, and MITRE ATT&CK; advised on data residency, privacy-by-design, and GRC.
|
||||
• Implemented DevSecOps and continuous compliance practices: secure CI/CD, IaC (Terraform/CloudFormation), container/Kubernetes security, SAST/DAST/SCA, and security KPI/telemetry reporting.
|
||||
• Architected AI/LLM security posture and delivery approach: AI footprint mapping, AI-specific threat modeling, guardrails/safety, governance, and observability—supporting secure RAG and agentic patterns and aligning to NIST AI RMF.
|
||||
• Designed data security and governance for modern analytics platforms (Purview, Databricks, Snowflake/BigQuery patterns), including cataloging, access control, and sensitive-data handling across pipelines.
|
||||
• Delivered IAM/PAM and identity governance work (CyberArk, Okta/Entra ID patterns, access reviews), supporting least privilege and privileged session controls.
|
||||
• Supported co-managed SOC / MSSP transitions by defining operating models, SLAs, metrics, reporting, and Tier-3 incident investigation processes.
|
||||
• Communicated risk and architecture decisions to executives and technical teams via briefings, risk registers, and hands-on enablement workshops.
|
||||
|
||||
- company: HPE / DXC (SOC / MSSP)
|
||||
position: Security Incident Analyst (Tier 2/3)
|
||||
timeperiod: June 2015 - August 2017
|
||||
remote: false
|
||||
employment: full-time
|
||||
location: Toronto, ON, Canada
|
||||
description: |
|
||||
• Led incident triage, investigation, and remediation across financial and public-sector environments using ArcSight and network/app security controls (IDS/WAF).
|
||||
• Developed SIEM content (queries, dashboards, correlation use cases) and incident playbooks; delivered RCA reporting and executive recommendations.
|
||||
• Performed threat hunting and forensics (pcap analysis, IOC enrichment, CTI workflows) and automated analysis tasks with scripting.
|
||||
|
||||
- company: Metsuke
|
||||
position: Security Consultant / Architect
|
||||
timeperiod: February 2012 - May 2015
|
||||
remote: hybrid
|
||||
employment: contract
|
||||
location: Toronto, ON, Canada
|
||||
description: |
|
||||
• Delivered security architecture, IAM/PAM remediation, and risk/vulnerability assessments for enterprise and public-sector clients.
|
||||
• Implemented secure CI/CD practices and integration hardening across infrastructure and applications; produced TRAs/PIAs and compliance-aligned documentation.
|
||||
• Designed and supported SIEM/IAM programs and logging/monitoring improvements for operational resilience; authored SOPs and technical documentation for SOC/hosting teams.
|
||||
|
||||
- company: TD Bank
|
||||
position: Senior Security Specialist
|
||||
timeperiod: August 2010 - September 2011
|
||||
remote: false
|
||||
employment: full-time
|
||||
location: Toronto, ON, Canada
|
||||
description: |
|
||||
• Led remediation after SOX/PCI audits across access control, privileged access, logging/monitoring, and secure system design for legacy enterprise platforms.
|
||||
• Supported SIEM deployment and compliance reporting; improved audit trails and incident readiness across on-prem environments.
|
||||
• Partnered with audit, infrastructure, and application teams to translate requirements into sustainable controls and operating procedures.
|
||||
|
||||
- company: Earlier Career (Selected)
|
||||
position: Security Architect / Developer / Manager (multiple roles)
|
||||
timeperiod: 1998 - 2010
|
||||
description: |
|
||||
• Security architecture, consulting, and leadership roles across ISPs, financial services, and technology firms (including Research In Motion/BlackBerry, Q1 Labs/IBM QRadar early work, N-Dimension SCADA security integration, and others).
|
||||
• Led/owned projects spanning incident response, vulnerability assessment, secure platform engineering, IAM, and governance artifacts aligned with ISO/NIST/OWASP and regulated environments.
|
||||
• IT/OT and SCADA security integration work (Modbus/DNP3, ruggedized platforms, secure gateways, and monitoring) supporting critical infrastructure and utility contexts.
|
||||
|
||||
education:
|
||||
- degree: MS Diploma (evaluated by York University, 2002)
|
||||
description: Moscow Institute of Electronic Techniques
|
||||
|
||||
certifications:
|
||||
- name: CISSP
|
||||
- name: "Cloud Security Alliance — Agentic AI Security Summit (2025)"
|
||||
- name: "Cloud Security Alliance — Virtual Cloud Non-Human Identities (NHI) Summit (2025)"
|
||||
- name: "LangChain Academy — Introduction to LangGraph (2025)"
|
||||
- name: "DND/CAF Architecture Framework (DNDAF) & QualiWare EA Toolset (2023)"
|
||||
- name: "ISC2 — Incident Management: Preparation and Response (2020)"
|
||||
- name: "ISC2 — DevSecOps: Integrating Security into DevOps (2018)"
|
||||
- name: "ArcSight ESM / SmartConnector Foundations & Administration (2016)"
|
||||
- name: "Ongoing professional training (LinkedIn Learning; see LinkedIn profile for full list)"
|
||||
|
||||
skills:
|
||||
- name: "Security architecture: Zero Trust, SSE/SASE, network & endpoint security, cloud-native security, security reference architectures"
|
||||
level: 96
|
||||
- name: "Cloud platforms: AWS, Azure, GCP (IAM, network segmentation, logging, workload protection, KMS/secrets, cloud posture management)"
|
||||
level: 92
|
||||
- name: "SecOps & tooling: SIEM/SOAR, detection engineering, incident response, threat hunting, TI integration (ArcSight, Splunk, Sentinel/KQL, QRadar, Elastic, LogRhythm, Datadog; Cortex/XSIAM)"
|
||||
level: 94
|
||||
- name: "DevSecOps & security testing: secure CI/CD (Jenkins/GitLab/GitHub), IaC (Terraform/CloudFormation), Kubernetes/Docker security, SAST/DAST/SCA, SBOM, WAF/API testing (Burp/Postman/Nmap/Nessus/Qualys/gotestwaf), policy-as-code (OPA)"
|
||||
level: 92
|
||||
- name: "Identity & privileged access: IGA, RBAC/ABAC, SSO (SAML/OIDC/OAuth2), directory services (AD/LDAP), PAM (CyberArk), access reviews and lifecycle controls"
|
||||
level: 90
|
||||
- name: "Risk, privacy & compliance: NIST CSF/800-53, CIS Controls, ISO 27001/27701/42001, ITSG-33, STRIDE/PASTA, MAESTRO, MITRE ATT&CK/ATLAS/D3FEND, privacy-by-design, TRA/PIA"
|
||||
level: 92
|
||||
- name: "AI / LLM security: NIST AI RMF, OWASP Top 10 for LLM/Agentic AI, secure RAG/agentic patterns, model/data protection, guardrails, evals/observability, jailbreak prevention and red teaming"
|
||||
level: 88
|
||||
- name: "AI governance & regulation: EU AI Act, Bill C-27, GDPR; model risk management, data residency, and compliance-by-design for AI/data platforms"
|
||||
level: 84
|
||||
- name: "Enterprise architecture: TOGAF, SABSA, Zachman; public-sector architecture practices (DNDAF familiarity) and traceable architecture decisions"
|
||||
level: 82
|
||||
- name: "Data & platforms: Snowflake, Databricks, Microsoft Purview, BigQuery, Synapse/Fabric patterns; lakehouse/medallion concepts; governance, cataloging, and access control"
|
||||
level: 82
|
||||
- name: "Programming & automation: Python, Bash, Go; JSON/YAML; scripting for log processing, reporting, and security telemetry"
|
||||
level: 82
|
||||
|
||||
projects:
|
||||
- name: "Selected Case Studies (SOC / SecOps / Architecture)"
|
||||
description: |
|
||||
• Web-attack “false positive” triage (banking): validated “blocked” actions end-to-end across ArcSight + security controls, performed packet/header analysis to attribute traffic to an authorized third-party test; closed ticket with evidence and trained analysts to repeat the method.
|
||||
• Web-attack “true positive” validation (banking): confirmed distributed hostile sources, verified exploit applicability against the client’s patched stack, and supported containment by identifying/closing an exposed infrastructure path while documenting risk and residual exposure.
|
||||
• SSH brute-force investigation: traced an internal-source alert to a perimeter exposure by resolving NAT/logging ambiguity; escalated with clear evidence and drove closure of an internet-exposed SSH tunnel through stakeholder follow-ups.
|
||||
• Detection engineering QA: diagnosed missing alerts to a subtle filter mismatch (leading whitespace) by comparing connector/base-event fields to use-case logic; handed off a precise fix to the content engineering team.
|
||||
• Critical-infrastructure signature validation (SCADA/Modbus): recreated malicious traffic in a lab, identified errors in 3 IDS signatures that the industry had accepted, corrected them, and reported upstream—improving detection accuracy and establishing a repeatable validation approach.
|
||||
|
||||
knowledge: []
|
||||
hobbies: []
|
||||
contributions: []
|
||||
|
||||
contact:
|
||||
email: boris.levit@yahoo.com
|
||||
phone: +1 (416) 804-7520
|
||||
street:
|
||||
city: Toronto, ON M2R 3N8, Canada
|
||||
website: "https://www.linkedin.com/in/boris-levit-025a88"
|
||||
website_label: LinkedIn
|
||||
github: ""
|
||||
github_label: ""
|
||||
|
||||
lang: en
|
||||
`
|
||||
@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<div class="resume" id="template">
|
||||
<div class="resume ai-bw-tpl" :id="rootId" :style="bodyFontStyle">
|
||||
<div id="resume-header">
|
||||
<div id="header-left">
|
||||
<h2 id="position">{{person.position}}</h2>
|
||||
<h1 id="name">{{person.name.first}} {{person.name.last}}</h1>
|
||||
<div id="info-flex">
|
||||
<span id="email"><a :href='"mailto:" + person.contact.email'>
|
||||
<i class="fa fa-envelope" aria-hidden="true"></i>{{ person.contact.email }}</a></span>
|
||||
<span id="phone"><i class="fa fa-phone-square" aria-hidden="true"></i> {{person.contact.phone}}</span>
|
||||
<span v-if="person.contact.website" id="website"><a :href="contactLinks.website"><i class="fa fa-linkedin" aria-hidden="true"></i>{{ person.contact.website_label || person.contact.website }}</a></span>
|
||||
<span v-if="person.contact.github" id="github"><a :href="contactLinks.github"><i class="fa fa-code-fork" aria-hidden="true"></i>{{ person.contact.github_label || 'Git' }}</a></span>
|
||||
<span v-if="person.contact.github_profile && contactLinks.github_profile" id="github-profile"><a :href="contactLinks.github_profile"><i class="fa fa-github" aria-hidden="true"></i>{{ person.contact.github_profile_label || ('@' + String(person.contact.github_profile).replace(/^@/, '')) }}</a></span>
|
||||
<span v-if="person.contact.personal_site" id="personal-site"><a :href="contactLinks.personal_site"><i class="fa fa-globe" aria-hidden="true"></i>{{ person.contact.personal_site_label || person.contact.personal_site }}</a></span>
|
||||
<span v-if="person.birth && person.birth.location" id="location"><i class="fa fa-map-marker" aria-hidden="true"></i> {{person.birth.location}}</span>
|
||||
<div id="info-flex" aria-label="Contact information">
|
||||
<span
|
||||
v-for="(item, idx) in headerInfoItems()"
|
||||
:key="item.key"
|
||||
class="header-info-item">
|
||||
<!-- no separator -->
|
||||
<a v-if="item.href" :href="item.href">
|
||||
<i :class="item.icon" aria-hidden="true"></i>{{ item.text }}
|
||||
</a>
|
||||
<span v-else>
|
||||
<i :class="item.icon" aria-hidden="true"></i>{{ item.text }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="header-right">
|
||||
@ -20,9 +24,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="resume-about" v-if="person.about">
|
||||
<h2>{{ lang.about }}</h2>
|
||||
<p>{{person.about}}</p>
|
||||
<p v-if="person.core_strengths" class="core-strengths"><span class="core-strengths-label">Core strengths:</span> {{person.core_strengths}}</p>
|
||||
<h2 id="profile-title">Profile</h2>
|
||||
<div class="spacer"></div>
|
||||
<p v-html="renderBold(person.about)"></p>
|
||||
<p v-if="person.core_strengths" class="core-strengths">
|
||||
<span class="core-strengths-label">Core strengths:</span>
|
||||
<span v-html="renderBold(person.core_strengths)"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div id="resume-body">
|
||||
<div id="experience-container">
|
||||
@ -36,77 +44,121 @@
|
||||
v-for="item in workArrangementLegendItems()"
|
||||
:key="item.key"
|
||||
class="experience-legend-item">
|
||||
<i :class="item.icon" aria-hidden="true"></i>
|
||||
<i v-if="item.icon" :class="item.icon" aria-hidden="true"></i>
|
||||
<span v-if="item.text" class="experience-legend-text-icon" aria-hidden="true">{{ item.text }}</span>
|
||||
<span class="experience-legend-text">{{ item.label }}</span>
|
||||
</span>
|
||||
</p>
|
||||
<div class="spacer"></div>
|
||||
<div class="experience" v-for="experience in person.experience" :key="experience.company">
|
||||
<h2 class="company-row">
|
||||
<span class="company-primary">
|
||||
<span :class="['company', experience.sub_companies && 'company-section-label']">{{experience.company}}</span><!--
|
||||
--><span
|
||||
v-if="experienceWorkBadges(experience).length"
|
||||
class="exp-name-icon-gutter"
|
||||
aria-hidden="true"></span><!--
|
||||
--><span
|
||||
v-if="experienceWorkBadges(experience).length"
|
||||
class="experience-icons"
|
||||
aria-hidden="true">
|
||||
<span
|
||||
v-for="b in experienceWorkBadges(experience)"
|
||||
:key="b.key"
|
||||
class="exp-icon-cell">
|
||||
<i
|
||||
:class="b.icon"
|
||||
:title="b.label"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="experienceLocation(experience)" class="company-location">{{ experienceLocation(experience) }}</span>
|
||||
</h2>
|
||||
<p class="job-info" v-if="!experience.sub_companies">
|
||||
<span class="job-title">{{experience.position}}</span>
|
||||
<span class="experience-timeperiod">{{ experienceDateRange(experience) }}</span>
|
||||
</p>
|
||||
<p class="job-info sub-companies-line" v-if="experience.sub_companies && experience.sub_companies.length">
|
||||
<span class="sub-companies-list">
|
||||
<span
|
||||
v-for="(sub, si) in experience.sub_companies"
|
||||
:key="si"
|
||||
class="sub-company-entry">
|
||||
<span class="sub-company-name">{{ sub.name }}</span><!--
|
||||
<div :class="['experience', { 'experience-section': !experience.position && !experience.location && !experience.stack, 'experience-compact': !experience.position && (experience.location || experience.stack) }]" v-for="experience in person.experience" :key="experience.company">
|
||||
<template v-if="!experience.position && !experience.location && !experience.stack">
|
||||
<h2 class="section-subheading">
|
||||
{{experience.company}}<span v-if="experienceDateRange(experience)" class="section-subheading-date"> · {{experienceDateRange(experience)}}</span>
|
||||
</h2>
|
||||
<div class="spacer"></div>
|
||||
</template>
|
||||
<template v-else-if="!experience.position">
|
||||
<h2 class="company-row">
|
||||
<span class="company-primary">
|
||||
<span class="company">{{experience.company}}</span><!--
|
||||
--><span
|
||||
v-if="experienceWorkBadges(sub).length"
|
||||
class="sub-icon-gutter"
|
||||
v-if="experienceWorkBadges(experience).length"
|
||||
class="exp-name-icon-gutter"
|
||||
aria-hidden="true"></span><!--
|
||||
--><span
|
||||
v-for="b in experienceWorkBadges(sub)"
|
||||
:key="b.key"
|
||||
class="sub-icon-cell"
|
||||
aria-hidden="true"><i
|
||||
:class="b.icon"
|
||||
:title="b.label"></i></span>
|
||||
<span
|
||||
v-if="si < experience.sub_companies.length - 1"
|
||||
class="sub-company-sep"
|
||||
aria-hidden="true">·</span>
|
||||
v-if="experienceWorkBadges(experience).length"
|
||||
class="experience-icons"
|
||||
aria-hidden="true">
|
||||
<span
|
||||
v-for="b in experienceWorkBadges(experience)"
|
||||
:key="b.key"
|
||||
class="exp-icon-cell">
|
||||
<i
|
||||
:class="b.icon"
|
||||
:title="b.label"></i>
|
||||
</span>
|
||||
</span><!--
|
||||
--><span
|
||||
v-if="person.show_stack !== false && experience.stack"
|
||||
class="inline-stack"><span class="stack-brace">{</span> <span v-html="renderBold(experience.stack)"></span> <span class="stack-brace">}</span></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="experience-timeperiod">{{ experienceDateRange(experience) }}</span>
|
||||
</p>
|
||||
</h2>
|
||||
<p
|
||||
v-if="experienceDescriptionLines(experience).length"
|
||||
class="compact-description"><span v-html="renderBold(stripLeadingBullet(experienceDescriptionLines(experience)[0]))"></span></p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2 class="company-row">
|
||||
<span class="company-primary">
|
||||
<span :class="['company', experience.sub_companies && 'company-section-label']">{{experience.company}}</span><!--
|
||||
--><span
|
||||
v-if="experienceWorkBadges(experience).length"
|
||||
class="exp-name-icon-gutter"
|
||||
aria-hidden="true"></span><!--
|
||||
--><span
|
||||
v-if="experienceWorkBadges(experience).length"
|
||||
class="experience-icons"
|
||||
aria-hidden="true">
|
||||
<span
|
||||
v-for="b in experienceWorkBadges(experience)"
|
||||
:key="b.key"
|
||||
class="exp-icon-cell">
|
||||
<i
|
||||
:class="b.icon"
|
||||
:title="b.label"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="experienceLocation(experience)" class="company-location">{{ experienceLocation(experience) }}</span>
|
||||
</h2>
|
||||
<p class="job-info" v-if="!experience.sub_companies">
|
||||
<span class="job-title">{{experience.position}}</span>
|
||||
<span class="experience-timeperiod">{{ experienceDateRange(experience) }}</span>
|
||||
</p>
|
||||
<p class="job-info sub-companies-line" v-if="experience.sub_companies && experience.sub_companies.length">
|
||||
<span class="sub-companies-list">
|
||||
<span
|
||||
v-for="(sub, si) in experience.sub_companies"
|
||||
:key="si"
|
||||
class="sub-company-entry">
|
||||
<span class="sub-company-name">{{ sub.name }}</span><!--
|
||||
--><span
|
||||
v-if="experienceWorkBadges(sub).length"
|
||||
class="sub-icon-gutter"
|
||||
aria-hidden="true"></span><!--
|
||||
--><span
|
||||
v-for="b in experienceWorkBadges(sub)"
|
||||
:key="b.key"
|
||||
class="sub-icon-cell"
|
||||
aria-hidden="true"><i
|
||||
:class="b.icon"
|
||||
:title="b.label"></i></span>
|
||||
<span
|
||||
v-if="si < experience.sub_companies.length - 1"
|
||||
class="sub-company-sep"
|
||||
aria-hidden="true">·</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="experience-timeperiod">{{ experienceDateRange(experience) }}</span>
|
||||
</p>
|
||||
<p
|
||||
v-if="person.show_stack !== false && experience.stack"
|
||||
class="stack-line">
|
||||
<span class="stack-brace">{</span> <span v-html="renderBold(experience.stack)"></span> <span class="stack-brace">}</span>
|
||||
</p>
|
||||
</template>
|
||||
<ul
|
||||
v-if="experienceDescriptionLines(experience).length"
|
||||
v-if="experience.position && experienceDescriptionLines(experience).length"
|
||||
class="job-description-list">
|
||||
<li
|
||||
v-for="(line, idx) in experienceDescriptionLines(experience)"
|
||||
:key="idx"
|
||||
class="list-item-black">{{ stripLeadingBullet(line) }}</li>
|
||||
:class="['list-item-black', { 'bullet-aside': stripLeadingBullet(line).indexOf('Ongoing:') === 0 }]"><span v-html="renderBold(stripLeadingBullet(line))"></span></li>
|
||||
</ul>
|
||||
<ul v-if="experience.list" >
|
||||
<li v-for="(item, index) in experience.list" :key="index">
|
||||
<span class="list-item-black">
|
||||
{{item}}
|
||||
<span v-html="renderBold(item)"></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
@ -116,23 +168,21 @@
|
||||
<h2 id="projects-title">{{ lang.projects }}</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="project" v-for="project in person.projects" :key="project.name">
|
||||
<h2 class="project-name">{{ project.name }}</h2>
|
||||
<ul
|
||||
<h2 class="project-name"><span v-html="renderBold(project.name)"></span><!--
|
||||
--><span
|
||||
v-if="person.show_stack !== false && project.stack"
|
||||
class="inline-stack"><span class="stack-brace">{</span> <span v-html="renderBold(project.stack)"></span> <span class="stack-brace">}</span></span></h2>
|
||||
<p
|
||||
v-if="experienceDescriptionLines(project).length"
|
||||
class="job-description-list">
|
||||
<li
|
||||
v-for="(line, idx) in experienceDescriptionLines(project)"
|
||||
:key="idx"
|
||||
class="list-item-black">{{ stripLeadingBullet(line) }}</li>
|
||||
</ul>
|
||||
class="compact-description"><span v-html="renderBold(stripLeadingBullet(experienceDescriptionLines(project)[0]))"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="education-container" v-if="person.education && person.education.length">
|
||||
<h2 id="education-title">{{ lang.education }}</h2>
|
||||
<div class="spacer"></div>
|
||||
<div class="education" v-for="education in person.education" :key="education.degree">
|
||||
<h2 class="education-description">{{education.description}}</h2>
|
||||
<p><span class="degree">{{education.degree}} | </span><span class="education-timeperiod">{{education.timeperiod}}</span></p>
|
||||
<h2 class="education-description"><span v-html="renderBold(education.description)"></span></h2>
|
||||
<p><span class="degree"><span v-html="renderBold(education.degree)"></span> | </span><span class="education-timeperiod">{{education.timeperiod}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="certifications-container" v-if="person.certifications && person.certifications.length">
|
||||
@ -142,7 +192,7 @@
|
||||
<li
|
||||
v-for="(c, cidx) in person.certifications"
|
||||
:key="cidx"
|
||||
class="list-item-black">{{ certificationLabel(c) }}</li>
|
||||
class="list-item-black"><span v-html="renderBold(certificationLabel(c))"></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="skills-container" v-if="person.skills && person.skills.length">
|
||||
@ -155,10 +205,19 @@
|
||||
<div
|
||||
v-for="skill in person.skills"
|
||||
:key="skill.name"
|
||||
class="skill-cell">{{ skill.name }}</div>
|
||||
class="skill-cell">
|
||||
<template v-if="skillParts(skill.name).label">
|
||||
<span class="skill-label"><span v-html="renderBold(skillParts(skill.name).label)"></span>:</span>
|
||||
<span class="skill-text"> <span v-html="renderBold(skillParts(skill.name).text)"></span></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-html="renderBold(skill.name)"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -169,7 +228,78 @@ import { getVueOptions } from './options';
|
||||
const name = 'ai-bw';
|
||||
const baseOptions = getVueOptions(name);
|
||||
export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
props: {
|
||||
bodyFont: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
rootId: {
|
||||
type: String,
|
||||
default: 'template'
|
||||
}
|
||||
},
|
||||
computed: Object.assign({}, baseOptions.computed || {}, {
|
||||
bodyFontStyle () {
|
||||
if (!this.bodyFont) return {};
|
||||
return { fontFamily: this.bodyFont };
|
||||
}
|
||||
}),
|
||||
methods: Object.assign({}, baseOptions.methods || {}, {
|
||||
headerInfoItems () {
|
||||
const items = [];
|
||||
const push = (key, icon, text, href) => {
|
||||
const t = (text === undefined || text === null) ? '' : String(text).trim();
|
||||
if (!t) return;
|
||||
items.push({
|
||||
key,
|
||||
icon,
|
||||
text: t,
|
||||
href: href || ''
|
||||
});
|
||||
};
|
||||
|
||||
const c = (this.person && this.person.contact) ? this.person.contact : {};
|
||||
const links = this.contactLinks || {};
|
||||
|
||||
push('email', 'fa fa-envelope', c.email, links.email);
|
||||
push('phone', 'fa fa-phone-square', c.phone, links.phone);
|
||||
if (c.website && links.website) {
|
||||
push('website', 'fa fa-linkedin', c.website_label || c.website, links.website);
|
||||
}
|
||||
if (c.github && links.github) {
|
||||
push('github', 'fa fa-code-fork', c.github_label || 'Git', links.github);
|
||||
}
|
||||
if (c.github_profile && links.github_profile) {
|
||||
const u = String(c.github_profile).trim().replace(/^@/, '');
|
||||
push('github_profile', 'fa fa-github', c.github_profile_label || ('@' + u), links.github_profile);
|
||||
}
|
||||
if (c.personal_site && links.personal_site) {
|
||||
push('personal_site', 'fa fa-globe', c.personal_site_label || c.personal_site, links.personal_site);
|
||||
}
|
||||
if (this.person && this.person.birth && this.person.birth.location) {
|
||||
push('location', 'fa fa-map-marker', this.person.birth.location, '');
|
||||
}
|
||||
if (this.person && this.person.work_auth) {
|
||||
push('work_auth', 'header-emoji-flag', this.person.work_auth, '');
|
||||
}
|
||||
return items;
|
||||
},
|
||||
escapeHtml (value) {
|
||||
const s = (value === undefined || value === null) ? '' : String(value);
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
renderBold (value) {
|
||||
const base = this.escapeHtml(value);
|
||||
if (!base) return '';
|
||||
let out = base.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
out = out.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
return out;
|
||||
},
|
||||
experienceLocation (exp) {
|
||||
const loc = exp && exp.location;
|
||||
return (loc !== undefined && loc !== null && String(loc).trim()) ? String(loc).trim() : '';
|
||||
@ -192,13 +322,35 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
return lines;
|
||||
},
|
||||
stripLeadingBullet (line) {
|
||||
return String(line).replace(/^[•*-]\s*/, '').trim();
|
||||
return String(line).replace(/^(?:[•-]|\*(?!\*))\s*/, '').trim();
|
||||
},
|
||||
certificationLabel (c) {
|
||||
if (c === undefined || c === null) return '';
|
||||
if (typeof c === 'string') return String(c).trim();
|
||||
const n = c.name;
|
||||
return (n !== undefined && n !== null && String(n).trim()) ? String(n).trim() : '';
|
||||
},
|
||||
skillParts (name) {
|
||||
const raw = (name === undefined || name === null) ? '' : String(name);
|
||||
const idx = raw.indexOf(':');
|
||||
if (idx <= 0) {
|
||||
return {
|
||||
label: '',
|
||||
text: raw.trim()
|
||||
};
|
||||
}
|
||||
const label = raw.slice(0, idx).trim();
|
||||
const text = raw.slice(idx + 1).trim();
|
||||
if (!label || !text) {
|
||||
return {
|
||||
label: '',
|
||||
text: raw.trim()
|
||||
};
|
||||
}
|
||||
return {
|
||||
label,
|
||||
text
|
||||
};
|
||||
}
|
||||
})
|
||||
}));
|
||||
@ -209,9 +361,14 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
@ink: #111;
|
||||
@muted: #444;
|
||||
@pad-x: 24px;
|
||||
#template {
|
||||
.ai-bw-tpl {
|
||||
box-sizing: border-box;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
|
||||
/* Disable ligatures so PDF rendering doesn't drop ti/ff/fi glyphs
|
||||
(was producing "So ware" / "le -shi QA" / "dra s" in print). */
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0, "clig" 0, "dlig" 0, "calt" 0;
|
||||
text-rendering: optimizeLegibility;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
@ -229,12 +386,12 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
color: @ink;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
a {
|
||||
@ -251,6 +408,8 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
background: #fff;
|
||||
border-bottom: 2px solid @ink;
|
||||
padding: 14px @pad-x 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
#header-left {
|
||||
width: 100%;
|
||||
@ -263,7 +422,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
line-height: 1.15;
|
||||
}
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: @muted;
|
||||
font-weight: 400;
|
||||
}
|
||||
@ -271,7 +430,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
gap: 4px 12px;
|
||||
|
||||
span {
|
||||
@ -280,40 +439,50 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
|
||||
i {
|
||||
color: @ink;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.header-emoji-flag {
|
||||
font-style: normal;
|
||||
filter: grayscale(1) contrast(3);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
&::before {
|
||||
content: '🇨🇦';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#resume-about {
|
||||
flex-shrink: 0;
|
||||
padding: 6px @pad-x 8px;
|
||||
padding: 6px @pad-x 4px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
box-sizing: border-box;
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
#profile-title {
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
h2, p {
|
||||
color: @ink;
|
||||
}
|
||||
p {
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.38;
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.core-strengths {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.38;
|
||||
white-space: normal;
|
||||
}
|
||||
@ -328,17 +497,20 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px @pad-x 8px;
|
||||
padding: 6px @pad-x 6px;
|
||||
background: #fff;
|
||||
|
||||
#experience-title, #education-title, #certifications-title, #skills-title, #projects-title {
|
||||
font-size: 12px;
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#experience-container {
|
||||
border-top: 1px solid #999;
|
||||
padding-top: 6px;
|
||||
margin-top: 8px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
@ -352,7 +524,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
display: block;
|
||||
margin: 0 0 2px 0;
|
||||
padding: 0;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: @muted;
|
||||
max-width: 100%;
|
||||
@ -378,7 +550,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
i {
|
||||
display: inline-block;
|
||||
color: @ink;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
width: 1.25em;
|
||||
min-width: 1.25em;
|
||||
margin-right: 5px;
|
||||
@ -393,25 +565,107 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.experience-legend-text-icon {
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: @ink;
|
||||
width: 1.25em;
|
||||
min-width: 1.25em;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
vertical-align: baseline;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.experience {
|
||||
margin: 0 0 9px 0;
|
||||
margin: 0 0 7px 0;
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
& + .experience {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
&.experience-section {
|
||||
border-top: none;
|
||||
padding-top: 8px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
break-after: avoid;
|
||||
page-break-after: avoid;
|
||||
.spacer {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.job-description-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
li {
|
||||
padding-left: 0;
|
||||
}
|
||||
li:nth-child(even) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 3px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.experience.experience-compact {
|
||||
margin: 0;
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
.company-row {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
.experience.experience-compact + .experience.experience-compact {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
.experience.experience-section + .experience.experience-compact {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.inline-stack {
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
color: @muted;
|
||||
margin-left: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.stack-brace {
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
color: @ink;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Grid keeps location in the right column; flex-wrap was sending it to a full-width row under bullets */
|
||||
.company-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(min-content, max-content);
|
||||
column-gap: 10px;
|
||||
align-items: baseline;
|
||||
margin: 0 0 5px 0;
|
||||
margin: 0 0 3px 0;
|
||||
line-height: 1.25;
|
||||
.company-primary {
|
||||
min-width: 0;
|
||||
@ -421,7 +675,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
.company {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
}
|
||||
@ -445,7 +699,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
i {
|
||||
color: @ink;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
opacity: 0.88;
|
||||
@ -454,7 +708,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
.company-location {
|
||||
justify-self: end;
|
||||
align-self: baseline;
|
||||
font-size: 10px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 400;
|
||||
color: @muted;
|
||||
line-height: 1.25;
|
||||
@ -475,7 +729,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(min-content, max-content);
|
||||
column-gap: 10px;
|
||||
align-items: baseline;
|
||||
margin: 0 0 5px 0;
|
||||
margin: 0 0 3px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@ -485,9 +739,9 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
list-style-position: outside;
|
||||
list-style-type: disc;
|
||||
li {
|
||||
font-size: 10px;
|
||||
line-height: 1.32;
|
||||
margin: 0 0 1px 0;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.28;
|
||||
margin: 0;
|
||||
padding-left: 1px;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
@ -497,8 +751,42 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
}
|
||||
|
||||
.job-title, .degree {
|
||||
.stack-line {
|
||||
margin: 0 0 3px 0;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.3;
|
||||
color: @muted;
|
||||
font-style: italic;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
|
||||
.section-subheading {
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.section-subheading-date {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: @muted;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.bullet-aside {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
font-weight: 500;
|
||||
color: @ink;
|
||||
font-size: 11px;
|
||||
min-width: 0;
|
||||
@ -507,12 +795,22 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.degree {
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.experience-timeperiod {
|
||||
justify-self: end;
|
||||
align-self: baseline;
|
||||
font-weight: 400;
|
||||
color: @muted;
|
||||
font-size: 10px;
|
||||
font-size: 10.5px;
|
||||
line-height: 1.3;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
@ -532,7 +830,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
.sub-company-name {
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sub-icon-gutter {
|
||||
@ -545,7 +843,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
margin-right: 5px;
|
||||
i {
|
||||
color: @ink;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
opacity: 0.88;
|
||||
@ -563,7 +861,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
.education-timeperiod {
|
||||
font-weight: 400;
|
||||
color: @muted;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.education {
|
||||
@ -571,7 +869,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
|
||||
#skills-container {
|
||||
margin-top: 14px;
|
||||
margin-top: 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
@ -581,7 +879,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
|
||||
#skill-description {
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
color: @muted;
|
||||
margin: 0 0 2px 0;
|
||||
@ -594,8 +892,8 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
|
||||
#skill-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
column-gap: 0;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 14px;
|
||||
row-gap: 3px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -604,7 +902,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
|
||||
.skill-cell {
|
||||
font-size: 9px;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.28;
|
||||
color: @ink;
|
||||
min-width: 0;
|
||||
@ -617,8 +915,18 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.skill-label {
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.skill-text {
|
||||
font-weight: 400;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
#projects-container {
|
||||
margin-top: 14px;
|
||||
margin-top: 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
@ -628,7 +936,18 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
|
||||
.project {
|
||||
margin: 0 0 4px 0;
|
||||
margin: 0;
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
& + .project {
|
||||
margin-top: 6px;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -637,21 +956,32 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
.company-section-label {
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: @muted;
|
||||
}
|
||||
|
||||
.compact-description {
|
||||
font-size: 11.5px;
|
||||
line-height: 1.15;
|
||||
color: @ink;
|
||||
margin: 0 0 -1px 0;
|
||||
padding-left: 1.1em;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
margin: 0 0 2px 0;
|
||||
margin: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#education-container {
|
||||
margin-top: 14px;
|
||||
margin-top: 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
@ -661,7 +991,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
|
||||
#certifications-container {
|
||||
margin-top: 14px;
|
||||
margin-top: 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
@ -676,7 +1006,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
list-style-position: outside;
|
||||
list-style-type: disc;
|
||||
li {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 2px 0;
|
||||
padding-left: 2px;
|
||||
@ -691,7 +1021,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 4mm 5mm 5mm 5mm;
|
||||
margin: 7mm 5mm 5mm 5mm;
|
||||
}
|
||||
|
||||
@page :first {
|
||||
@ -702,7 +1032,7 @@ export default Vue.component(name, Object.assign({}, baseOptions, {
|
||||
.spacer {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #999;
|
||||
margin: 4px 0 8px;
|
||||
margin: 3px 0 6px;
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -199,6 +199,12 @@ function getVueOptions (name) {
|
||||
key: 'coop',
|
||||
icon: 'fa fa-graduation-cap',
|
||||
label: 'Co-op'
|
||||
},
|
||||
{
|
||||
key: 'stack',
|
||||
icon: '',
|
||||
text: '{ }',
|
||||
label: 'Stack'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user