// Lebenslauf CV, a Typst resume template.
//
// Tested with Typst 0.14.2.
// Set in Open Sans. Without it installed the text falls back;
// change the font argument in the show rule below to a family you
// do have, the layout does not depend on it.
//
// Licence: MIT-0. Use it, change it, publish the result as your own CV.
// No attribution has to travel with the document you make from it.
//
// Self-contained: no imports, nothing to install.
// Template library: https://github.com/davidculemann/typst-templates
// Edit in a browser: https://www.jobsprout.ai/resume-templates/lebenslauf
#set page(fill: rgb("#ffffff"))

#let js-emergency-wrap(body) = layout(size => {
  show regex("\\S+(?:[ \\t]+\\S+)*"): value => context {
    let wrap-token(token) = {
      if measure(token).width > size.width {
        let segments = token.matches(regex("[^/.:@_?&=#%+‐‑–-]+|[/.:@_?&=#%+‐‑–-]+"))
        let emergency = segments.any(segment => {
          let natural = measure(segment.text)
          natural.width > size.width and measure(segment.text, width: size.width).height <= natural.height + 0.01pt
        })
        if emergency {
          let lines = ()
          let line = ""
          for segment in segments {
            let natural = measure(segment.text)
            if measure(line + segment.text).width <= size.width {
              line += segment.text
            } else if natural.width <= size.width {
              if line != "" { lines.push(line) }
              line = segment.text
            } else {
              if line != "" { lines.push(line); line = "" }
              if measure(segment.text, width: size.width).height > natural.height + 0.01pt {
                lines.push(segment.text)
              } else {
                for cluster in segment.text.clusters() {
                  if line != "" and measure(line + cluster).width > size.width {
                    lines.push(line)
                    line = cluster
                  } else { line += cluster }
                }
              }
            }
          }
          if line != "" { lines.push(line) }
          lines.join(linebreak())
        } else { token }
      } else { token }
    }
    let tokens = if measure(value).width > size.width { value.text.matches(regex("\\S+|\\s+")) } else { () }
    let parts = tokens.map(token => if token.text.contains(regex("\\S")) { wrap-token(token.text) } else { token.text })
    if parts.any(part => type(part) != str) { parts.join() } else { value }
  }
  body
})

#let js-header-flow(body) = layout(size => {
  if measure(body).width <= size.width { body } else { js-emergency-wrap(body) }
})

#let js-header-capacity(size) = {
  if page.height == auto { size.height } else {
    let height = page.height.to-absolute()
    let default-margin = calc.min(page.width, page.height) * (2.5 / 21)
    let margin = page.margin
    let edge(name) = {
      let value = if type(margin) == dictionary {
        margin.at(name, default: margin.at("y", default: margin.at("rest", default: auto)))
      } else { margin }
      if value == auto { default-margin }
      else if type(value) == ratio { value * height }
      else if type(value) == length { value.to-absolute() }
      else { value.ratio * height + value.length.to-absolute() }
    }
    calc.min(size.height, height - edge("top") - edge("bottom"))
  }
}

#let js-header-block(..args) = context {
  let named = args.named()
  let body = args.pos().first()
  let outer = (
    above: named.at("above", default: block.above),
    below: named.at("below", default: block.below),
    sticky: named.at("sticky", default: block.sticky),
  )
  named.insert("above", 0pt)
  named.insert("below", 0pt)
  block(..outer, layout(size => {
    let fitted = named
    if measure(block(..named, body), width: size.width).height > js-header-capacity(size) {
      fitted.insert("breakable", true)
      fitted.insert("sticky", false)
    }
    block(..fitted, body)
  }))
}

#let js-entry-frame(header: [], body: []) = layout(size => {
  let prepared = { set block(spacing: 0pt); set par(spacing: 0pt); header }
  let fits = measure(prepared, width: size.width).height <= js-header-capacity(size)
  block(breakable: true, {
    set block(spacing: 0pt)
    set par(spacing: 0pt)
    block(sticky: fits, breakable: not fits, header)
    body
  })
})

#let js-keep-entry(body) = block(breakable: true, layout(size => {
  let fits = measure(body, width: size.width).height <= js-header-capacity(size)
  block(above: 0pt, below: 0pt, breakable: not fits, body)
}))

#let js-header-cell-widths(columns, cells, gutter, width, cell-function, rules: ()) = {
  let tracks = if type(columns) == array { columns } else if type(columns) == int {
    range(columns).map(_ => auto)
  } else { (columns,) }
  if tracks.len() == 0 { tracks = (auto,) }
  let natural = tracks.map(_ => 0pt)
  let index = 0
  for cell in cells {
    if type(cell) == content and rules.contains(cell.func()) { continue }
    let body = if type(cell) == content and cell.func() == cell-function { cell.body } else { cell }
    let column = calc.rem(index, tracks.len())
    natural.at(column) = calc.max(natural.at(column), measure(body).width)
    index += 1
  }
  let gap = if type(gutter) == array { gutter.at(0, default: 0pt) } else { gutter }
  let available = calc.max(0pt, width - gap.to-absolute() * (tracks.len() - 1))
  let widths = tracks.enumerate().map(((i, track)) => {
    if track == auto { calc.min(natural.at(i), available) }
    else if type(track) == fraction { 0pt }
    else if type(track) == ratio { track * width }
    else { track.to-absolute() }
  })
  let remaining = calc.max(0pt, available - widths.sum())
  let fractions = tracks.filter(track => type(track) == fraction).map(track => track / 1fr).sum(default: 0)
  if fractions > 0 {
    for (i, track) in tracks.enumerate() {
      if type(track) == fraction { widths.at(i) = remaining * (track / 1fr) / fractions }
    }
  }
  widths
}

#let js-header-cells(cells, widths, cell-function, rules: ()) = {
  let result = ()
  let index = 0
  for cell in cells {
    if type(cell) == content and rules.contains(cell.func()) { result.push(cell); continue }
    let width = widths.at(calc.rem(index, widths.len()))
    if type(cell) == content and cell.func() == cell-function {
      if measure(cell.body).width <= width { result.push(cell) } else {
        let fields = cell.fields()
        fields.remove("body")
        result.push(cell-function(..fields, js-emergency-wrap(cell.body)))
      }
    } else if measure(cell).width <= width { result.push(cell) } else { result.push(js-emergency-wrap(cell)) }
    index += 1
  }
  result
}

#let js-header-stack(cells, gap: 0.2em) = {
  set par(spacing: 0pt, justify: false)
  set block(spacing: 0pt)
  for (i, cell) in cells.enumerate() {
    block(above: if i == 0 { 0pt } else { gap }, breakable: true, {
      set text(bottom-edge: "descender")
      let body = if type(cell) == content and (cell.func() == grid.cell or cell.func() == table.cell) { cell.body } else { cell }
      js-emergency-wrap(body)
    })
  }
}

#let js-header-starved(cells, widths, cell-function, rules: ()) = {
  let index = 0
  let starved = false
  for cell in cells {
    if type(cell) == content and rules.contains(cell.func()) { continue }
    let body = if type(cell) == content and cell.func() == cell-function { cell.body } else { cell }
    if widths.at(calc.rem(index, widths.len())) <= 0pt and measure(body).width > 0pt { starved = true }
    index += 1
  }
  starved
}

#let js-header-grid(..args) = layout(size => {
  set text(hyphenate: false)
  set par(justify: false)
  let named = args.named()
  let rules = (grid.hline, grid.vline)
  let widths = js-header-cell-widths(
    named.at("columns", default: 1), args.pos(),
    named.at("column-gutter", default: named.at("gutter", default: 0pt)),
    size.width, grid.cell, rules: rules,
  )
  if js-header-starved(args.pos(), widths, grid.cell, rules: rules) {
    js-header-stack(args.pos().filter(cell => type(cell) != content or not rules.contains(cell.func())))
  } else {
    grid(..named, ..js-header-cells(args.pos(), widths, grid.cell, rules: rules))
  }
})

#let js-header-table(..args) = layout(size => {
  set text(hyphenate: false)
  set par(justify: false)
  let named = args.named()
  let rules = (table.hline, table.vline, table.header, table.footer)
  let widths = js-header-cell-widths(
    named.at("columns", default: 1), args.pos(),
    named.at("column-gutter", default: named.at("gutter", default: 0pt)),
    size.width, table.cell, rules: rules,
  )
  if js-header-starved(args.pos(), widths, table.cell, rules: rules) {
    js-header-stack(args.pos().filter(cell => type(cell) != content or not rules.contains(cell.func())))
  } else {
    table(..named, ..js-header-cells(args.pos(), widths, table.cell, rules: rules))
  }
})

#let js-reading-order-header(cells: (), gutter: 0.6em) = {
  set par(justify: false)
  layout(size => {
    let grid-cells = cells.slice(0, calc.min(cells.len(), 4))
    let left-aligned = ()
    let right-aligned = ()
    for (i, cell) in grid-cells.enumerate() {
      if calc.even(i) { left-aligned.push(cell) } else { right-aligned.push(cell) }
    }
    let left-body = left-aligned.join(linebreak())
    let right-body = right-aligned.join(linebreak())
    let free = size.width - gutter.to-absolute()
    let left-width = measure(left-body).width
    let right-width = measure(right-body).width
    let paired = right-width <= free * 0.3 or left-width + right-width <= free
    if right-aligned.len() == 0 {
      js-header-grid(columns: (auto, 1fr), align: (left + top, right + top), left-body, [])
    } else if paired {
      js-header-grid(
        columns: (1fr, right-width),
        align: (left + top, right + top),
        column-gutter: gutter,
        left-body,
        right-body,
      )
    } else {
      js-header-stack(grid-cells)
    }
    if cells.len() > 4 {
      for cell in cells.slice(4) { block(above: 0.3em, js-emergency-wrap(cell)) }
    }
  })
}
// Shared predicate for values that render nothing. Apply BEFORE wrapping a
// value in strong()/emph()/text(): styled empty content is not blank.
// Byte-synced with TYPST_IS_BLANK in utils/typst-blocks.ts (typst-codegen.test.ts).
#let is-blank(v) = v == none or v == "" or v == []
// Masthead padding for CJK documents. Typst measures a `v()` after a line from
// the BASELINE and starts the next line's box at CAP-HEIGHT, both Latin metrics
// that a CJK font reports for its own Latin glyphs; kanji, kana and hangul rise
// above the one and sit below the other, so a gap tuned to clear "Quigley" over
// "Business Development Manager" left 0.5pt of white under a Japanese name. Reads the
// document's own language, so a Latin document emits the spacing it always did.
// Gated by masthead-clearance.test.ts, which measures both scripts.
#let cjk-langs = ("ja", "ko", "zh")
#let script-gap(base) = context v(base + if text.lang in cjk-langs { 3pt } else { 0pt })

// A text box that contains the ink it draws, which is what makes a cell inset a
// real visual gap rather than a nominal one.
//
// Same root cause as the masthead pad above: the default box runs cap-height to
// baseline, both Latin metrics, and kana and kanji rise above the one and sit
// below the other. A 0.12em inset on the furigana strip was consumed entirely and
// the reading printed onto the rule above it. "bounds" fits the box to the
// glyphs actually drawn, so it costs nothing on a line with no overshoot and
// needs no per-font constant. Gated by form-cell-clearance.test.ts.
#let ink-fit(body) = text(top-edge: "bounds", bottom-edge: "bounds", body)

// An original tabular Lebenslauf: a narrow date column beside every entry, the
// entry itself flowing in the wide column, and the masthead's optional photo
// and date of birth taking no room when they are absent.

// Sizes are steps of the body size B (cv-typography-standard rule 2), so the
// hierarchy survives the size knob. Entry titles use the body size with weight.
#let lb-display = 2.4em
#let lb-label = 0.9em
#let lb-meta = 0.82em

// The date column is 27mm and its gutter 5mm at the 10.5pt default. Both scale
// with the body size, so a compact date range still fits one line at 12pt.
#let lb-date-col = 27mm / 10.5pt * 1em
#let lb-col-gap = 5mm / 10.5pt * 1em
#let lb-indent = lb-date-col + lb-col-gap

// The portrait slot: 3:4, the conventional German application photo ratio.
#let lb-photo-width = 85pt
#let lb-photo-height = 113.4pt

// Vertical rhythm. Section and entry gaps derive from par.spacing, which the
// sectionSpacing knob rescales, so the knob can only ever scale a positive
// length. The section gap is weak spacing at the strongest level, so it wins
// over the previous entry's own gap and still collapses at the top of a page.
#let lb-section-gap() = context v(par.spacing * 1.25, weak: true)
#let lb-entry-gap() = par.leading + par.spacing * (0.5 / 1.2)
#let lb-label-gap = 0.65em

// A date set on the same baseline as the entry title beside it, whatever the
// meta size: its first line box starts at the title font's cap height.
#let lb-dates(dates, muted-color) = context {
  let cap = measure(text(weight: "bold", top-edge: "cap-height", bottom-edge: "baseline")[H]).height
  set par(justify: false)
  js-header-flow(text(size: lb-meta, fill: rgb(muted-color), top-edge: cap, hyphenate: false, dates))
}

#let lb-detail(item, muted-color) = {
  if item.kind == "subtitle" { item.body } else {
    text(size: lb-meta, fill: rgb(muted-color), item.body)
  }
}

#let lb-bullets(items) = context if page.height == auto {
  // Continuous-height measurement has no page boundary to protect.
  list(..items)
} else {
  layout(size => {
    let full-height = page.height - page.margin.top - page.margin.bottom
    let start = 0
    while start < items.len() {
      let remaining = items.len() - start
      let count = if remaining == 3 { 3 } else { calc.min(2, remaining) }
      let group = list(..items.slice(start, start + count))
      let fits = measure(group, width: size.width).height <= full-height
      if not fits and count > 1 {
        count = 1
        group = list(items.at(start))
        fits = measure(group, width: size.width).height <= full-height
      }
      if start > 0 { v(par.leading) }
      block(above: 0pt, below: 0pt, width: 100%, breakable: not fits, group)
      start += count
    }
  })
}

// Keep a compact section together only when its heading and entries fit a page.
#let lb-entry-group(header, body) = context if page.height == auto {
  body
} else {
  layout(size => {
    let full-height = page.height - page.margin.top - page.margin.bottom
    let fits = measure(header + body, width: size.width).height <= full-height
    block(above: 0pt, below: 0pt, width: 100%, breakable: not fits, body)
  })
}

// One entry: the dates in the left column, the title, its details in their
// stored order, and the body in the right. The row is breakable, so an entry
// taller than the page continues on the next one with nothing cut.
#let lb-entry(
  dates: none,
  title: none,
  details: (),
  muted-color: "#62676b",
  body: none,
) = context {
  let inner-gap = par.leading + 0.15em
  block(above: 0pt, below: lb-entry-gap(), width: 100%, breakable: true, grid(
    columns: (lb-date-col, 1fr),
    column-gutter: lb-col-gap,
    if is-blank(dates) { [] } else { lb-dates(dates, muted-color) },
    {
      set par(spacing: inner-gap)
      set block(spacing: inner-gap)
      let head = ()
      if not is-blank(title) { head.push(text(weight: "bold", hyphenate: false, title)) }
      let shown = details.filter(item => not is-blank(item.body))
      if shown.len() > 0 {
        head.push(shown.map(item => lb-detail(item, muted-color)).join([#sym.space.nobreak#text(fill: rgb(muted-color), sym.dot.c) ]))
      }
      if head.len() > 0 { js-header-block(width: 100%, sticky: true, js-header-flow(head.join(linebreak()))) }
      if not is-blank(body) { block(width: 100%, body) }
    },
  ))
}

// Section-level content without dates (profile, skills, languages) sits in the
// same right-hand column as every entry, so the page keeps one text edge.
#let lb-language(language: none, level: none, muted-color: "#62676b") = {
  if not is-blank(language) { text(weight: "bold", hyphenate: false, language) }
  if not is-blank(language) and not is-blank(level) { [:#sym.space.nobreak] }
  if not is-blank(level) { text(size: lb-meta, fill: rgb(muted-color), level) }
}

#let lb-aside(compact: false, body) = context block(
  above: 0pt,
  below: lb-entry-gap(),
  width: 100%,
  pad(left: lb-indent, {
    if compact {
      set par(spacing: par.leading)
      set block(spacing: par.leading)
      body
    } else { body }
  }),
)

// Section label in the accent, with a hairline running to the right margin.
#let lb-section(title, accent-color: "#214e54") = {
  lb-section-gap()
  block(above: 0pt, below: lb-label-gap, sticky: true, width: 100%, {
    set par(spacing: 0pt, justify: false)
    grid(
      columns: (auto, 1fr),
      column-gutter: 0.6em,
      align: (left + horizon, left + horizon),
      text(size: lb-label, weight: "bold", fill: rgb(accent-color), hyphenate: false, title),
      line(length: 100%, stroke: 0.5pt + rgb(accent-color).lighten(55%)),
    )
  })
}

#let lebenslauf(
  author: "",
  contacts: (),
  date-of-birth: none,
  dob-label: none,
  photo: none,
  accent-color: "#214e54",
  text-color: "#202326",
  muted-color: "#62676b",
  font: "Open Sans",
  font-size: 10.5pt,
  paper: "a4",
  margin-x: 20mm,
  margin-y: 18mm,
  leading: 0.65em,
  lang: "de",
  body,
) = {
  let author-str = if type(author) == str { author } else { "" }
  set document(
    title: if author-str == "" { none } else { author-str },
    author: if author-str == "" { () } else { author-str },
  )

  set text(
    font: font,
    size: font-size,
    lang: lang,
    fill: rgb(text-color),
    hyphenate: true,
    costs: (hyphenation: 100000%),
  )
  set par(justify: false, leading: leading, linebreaks: "optimized")
  show strong: set text(hyphenate: false)
  set list(indent: 0pt, body-indent: 0.55em, marker: text(fill: rgb(muted-color))[•])

  set page(
    paper: paper,
    margin: (x: margin-x, y: margin-y),
    footer: context {
      let total = counter(page).final().first()
      if total > 1 {
        set text(size: lb-meta, fill: rgb(muted-color), hyphenate: false)
        grid(
          columns: (1fr, auto),
          author, [#counter(page).display() / #total],
        )
      }
    },
  )

  let identity = {
    set par(spacing: 0pt, justify: false)
    if not is-blank(author) {
      block(above: 0pt, below: 0pt, width: 100%, {
        set text(size: lb-display, bottom-edge: "descender", hyphenate: false)
        set par(spacing: 0pt, leading: 0.2em)
        author
      })
      script-gap(0.45em)
    }
    let lines = ()
    if contacts.len() > 0 {
      lines.push(contacts.join([#sym.space.nobreak#sym.dot.c ]))
    }
    if not is-blank(date-of-birth) {
      lines.push([#text(fill: rgb(muted-color), dob-label)#sym.space.nobreak#text(fill: rgb(text-color), date-of-birth)])
    }
    if lines.len() > 0 {
      text(size: lb-meta, fill: rgb(muted-color), hyphenate: false, lines.join(linebreak()))
    }
  }

  block(above: 0pt, below: 0pt, width: 100%, breakable: false, {
    if photo != none {
      grid(
        columns: (1fr, lb-photo-width),
        column-gutter: lb-col-gap,
        align: (left + top, right + top),
        identity,
        box(
          width: lb-photo-width,
          height: lb-photo-height,
          clip: true,
          image(photo, width: lb-photo-width, height: lb-photo-height, fit: "cover"),
        ),
      )
    } else {
      identity
    }
  })

  body
}


#let smart-entry(threshold: 10%, body) = {
  block(breakable: false, height: threshold)
  v(-threshold, weak: true)
  body
}


#show: lebenslauf.with(
  author: "Hannah Mueller",
  accent-color: "#214e54",
  font: "Open Sans",
  font-size: 10.5pt,
  paper: "a4",
  leading: 0.65em,
  lang: "de",
  text-color: "#202326",
  muted-color: "#62676b",
  margin-x: 20mm,
  margin-y: 18mm,
  contacts: (link("mailto:hannah.mueller@example.com")[hannah.mueller\@example.com], [Berlin, Deutschland],),
)


#[
#set block(above: 0pt, below: 0pt)
#lb-section("Profil", accent-color: "#214e54")
#lb-aside(compact: false)[
Kommunikationswissenschaftlerin mit Erfahrung in der EU-Öffentlichkeitsarbeit, vom Blue-Book-Praktikum in Brüssel bis hin zu Jugendkampagnen in Berlin. Auf der Suche nach einer Position im Bereich Kommunikation oder Politikgestaltung bei einer EU-Institution oder NGO.
]
]

#[
#set block(above: 0pt, below: 0pt)
#lb-section("Berufserfahrung", accent-color: "#214e54")
#smart-entry[
#lb-entry(
  dates: "10.2023 - Heute",
  title: "Referentin für Presse- und Öffentlichkeitsarbeit",
  details: ((kind: "subtitle", body: "Stiftung Junges Europa"),),
  muted-color: "#62676b",
  body: [
#lb-bullets(([Betreuung der Kanäle der Stiftung auf drei Plattformen; Steigerung der Follower-Zahlen von 4.000 auf 19.000 innerhalb eines Jahres.], [Organisation von 12 Jugend-Bürgerforen in vier Regionen, wovon zwei in der nationalen Presse Erwähnung fanden.], [Verfassen und Platzieren von Pressemitteilungen auf Deutsch und Englisch; Erzielung von neun Veröffentlichungen in nationalen Medien.],))
],
)
]
#smart-entry[
#lb-entry(
  dates: "03.2023 - 07.2023",
  title: "Blue-Book-Praktikantin",
  details: ((kind: "subtitle", body: "Europäische Kommission, GD Kommunikation"),),
  muted-color: "#62676b",
  body: [
#lb-bullets(([Erstellung von Newsletter-Inhalten für eine Zielgruppe von 40.000 Abonnenten in sechs Sprachversionen.],))
],
)
]
#smart-entry[
#lb-entry(
  dates: "06.2020 - 09.2020",
  title: "Redaktionspraktikantin",
  details: ((kind: "subtitle", body: "Berliner Tagesblatt"),),
  muted-color: "#62676b",
  body: [
#lb-bullets(([Faktenprüfung der täglichen Berichterstattung und Verfassen von 14 veröffentlichten Kurzbeiträgen während eines Sommers.],))
],
)
]
]

#[
#set block(above: 0pt, below: 0pt)
#lb-section("Ausbildung", accent-color: "#214e54")
#smart-entry[
#lb-entry(
  dates: "09.2021 - 07.2022",
  title: "Hochschule für Europapolitik Brandenburg",
  details: ((kind: "subtitle", body: "Master of Arts Europäische Regierungslehre"),),
  muted-color: "#62676b",
  body: [
#lb-bullets(([Masterarbeit über die Kommunikation digitaler Politik durch EU-Institutionen an Bürger; Note 1,3.],))
],
)
]
#smart-entry[
#lb-entry(
  dates: "09.2017 - 06.2021",
  title: "Hochschule für Publizistik Berlin",
  details: ((kind: "subtitle", body: "Bachelor of Arts Journalismus"),),
  muted-color: "#62676b",
  body: [
#lb-bullets(([Erasmus-Jahr in Leuven, Belgien; Redakteurin der Fakultätszeitschrift.],))
],
)
]
]

#[
#set block(above: 0pt, below: 0pt)
#lb-section("Kenntnisse", accent-color: "#214e54")
#lb-aside(compact: true)[
Verfassen von Pressetexten, Social Media, Newsletter, InDesign, Canva, Web-Analyse, SEO, Videobearbeitung, WordPress
]
]

#[
#set block(above: 0pt, below: 0pt)
#lb-section("Zertifizierungen", accent-color: "#214e54")
#lb-entry-group([#lb-section("Zertifizierungen", accent-color: "#214e54")])[
#lb-entry(
  dates: none,
  title: "DELF B2",
  details: ((kind: "subtitle", body: "France Éducation international"),),
  muted-color: "#62676b",
  body: none,
)
#lb-entry(
  dates: none,
  title: "Google Digital Marketing Zertifikat",
  details: ((kind: "subtitle", body: "Google"),),
  muted-color: "#62676b",
  body: none,
)
#lb-entry(
  dates: none,
  title: "C1 Advanced (CAE)",
  details: ((kind: "subtitle", body: "Cambridge English"),),
  muted-color: "#62676b",
  body: none,
)
]
]

#[
#set block(above: 0pt, below: 0pt)
#lb-section("Sprachen", accent-color: "#214e54")
#lb-aside(compact: true)[
#lb-language(
  language: "Deutsch",
  level: "Muttersprache",
  muted-color: "#62676b",
), #lb-language(
  language: "Englisch",
  level: "Verhandlungssicher",
  muted-color: "#62676b",
), #lb-language(
  language: "Französisch",
  level: "Fließend",
  muted-color: "#62676b",
), #lb-language(
  language: "Spanisch",
  level: "Grundkenntnisse (A2)",
  muted-color: "#62676b",
)
]
]

